n-uf motion & placement
01 Window motion · How things move

Fast at the start, gentle at the end, never past the target.

This page compares how windows move in Hyprland, in a typical host app today, and under the proposed rules. The curves come from their real parameters: Hyprland's source, the host app's springs and tokens, and the proposal's launched, critically damped settle, stepped by this library. The dots are measured from a screen recording. Time runs left to right in milliseconds.

Built with @n-uf/motion (opens in a new tab) createSettle, SETTLE_TEMPO, reveal, createFrameLoop

Window motionSettle: one move from A to B

Settle: one move from A to B

Hyprland front-loads the move and never bounces. A host app's re-layout spring starts slowly, and its maximize/restore curve overshoots. The proposed settle starts fast like Hyprland, cannot overshoot, and has one tempo knob ω.

0501001502002503003504004500%25%50%75%90%100%mstarget

Up is progress from A to B; above the dashed target line means overshoot.

  • Measured: a real Hyprland window (dots)
  • Hyprland spring 'easy'
  • Hyprland older ease-out
  • Typical app: re-layout spring from rest
  • Typical app: maximize/restore (overshoots)
  • n-uf motion settle, quick ω 37
  • n-uf motion settle, default ω 25 library
  • n-uf motion settle, calm ω 20
Hyprland shipped: spring easy (ratio 0.785)
Hyprland older: easeOutQuint 479 ms
Host app rect spring 320, from rest
Host app maximize/restore: snappy 220 ms (y2 1.05)
Proposed settle ω=37 (recording)
Proposed settle ω=25 (default)
Proposed settle ω=20 (calm)
Recording: window open (measured)
t = 450 ms of 450 ms
Speed
Tempo
The dots follow the playhead

Each row beside the chart is the same curve as a moving block; dashed ticks mark the target. Speed slows the playback; Tempo picks the proposal's settle (calm 20, default 25, quick 37).

The cases

Four moves, frame by frame

Settle: one move from A to B

Hyprland front-loads the move and never bounces. A host app's re-layout spring starts slowly, and its maximize/restore curve overshoots. The proposed settle starts fast like Hyprland, cannot overshoot, and has one tempo knob ω.

Drag: pointer vs window, then drop

Held means held: the window is the pointer minus the grab offset in the same frame. The host app's own-drag spring (stiffness 1400) trails by about 50 ms. On release the window settles from where it was let go into its slot (drop at 260 ms).

Retarget mid-flight

A nearer target at 40 ms. The proposed settle relaunches from the drawn rect toward the nearer target, at its tempo times the distance left, so it can't overshoot it. A tween restart kinks the speed, and the host app's docked branch (and a space change) stops and restarts from rest.

A window shrinking 220 → 140 px wide

Frames at 0, 30, 60 and 120 ms after the change. Stretch (today, and Hyprland for ~40 ms): content is painted at the final size and scaled to the drawn box. Reveal (proposed): content is laid out at the final size and never scaled, and the frame's background band closes in.

The rules

Eight rules, four engines

What Hyprland does (from its source and a recording), what a tiling library and a host app do today, and what the proposal does: one settle for every move, held drags, reveal instead of stretch.

Held drag

Hyprland
warped 1:1; only 1 frame of capture latency
hypr-tiling today
separate ghost at 90%, hops to the seat
Host app today
localDrag spring ≈ 50 ms lag (live move holds docked panels only)
Proposed
hold: pointer − grab, same frame

Interactive resize

Hyprland
all tiles warped, content unscaled
hypr-tiling today
divider 1:1
Host app today
sprung and scaled
Proposed
hold + reveal

Reflow curve

Hyprland
90% ≈ 60 ms (recording) / 175–190 ms (shipped)
hypr-tiling today
170 ms CSS, only during drags; others jump
Host app today
stiffness 320 from rest: 90% at 218 ms, slow start
Proposed
settle ω=25: 90% at 92 ms, done at 184 ms

Overshoot on size

Hyprland
none seen (shipped spring allows 1.8%)
hypr-tiling today
none by default; swap dip to 0.7
Host app today
maximize/restore "snappy" y2 = 1.05
Proposed
impossible (launch + clamp)

Retarget

Hyprland
from drawn; spring keeps velocity
hypr-tiling today
restarts CSS transition; settle strip snaps
Host app today
spring keeps velocity; tweens, space changes and docked restart
Proposed
always from drawn; a new target relaunches toward it, so it never passes it

Content

Hyprland
stretched ~40–50 ms on reflow
hypr-tiling today
stretched the whole 170 ms
Host app today
stretched (non-docked), docked size jumps
Proposed
never stretched; uniform popin only for open/close

Open / close

Hyprland
popin 87% + fade, ~100 ms
hypr-tiling today
appear/vanish instantly
Host app today
fade + 8 px rise 180 ms / 140 ms
Proposed
popin 0.87 → 1 (uniform) + fade on settle

Neighbours during drag

Hyprland
reflow at once when the window lifts
hypr-tiling today
reservation slot keeps the gap
Host app today
old slot kept (live move)
Proposed
left to your layout; motion glides whatever rects it is given

The same settle moves docks and palettes on a desk and lands a panel after a drag.

FAQ

Questions

Why does the settle never overshoot its target?

Because createSettle follows distance · e^(−omega·t), a curve that only approaches its target. On every new target each component gets the velocity omega · distance toward it from wherever it is drawn, so there is no overshoot on position and no wobble on size, however often it is retargeted.

What are the settle's tempo values?

SETTLE_TEMPO has three: calm 20, default 25 and quick 37, as the omega in 1/s. They cover 90 % of the way in 115, 92 and 62 ms, then ease in to the target. Pass one as the omega option of createSettle or settleValue.

How do I respect prefers-reduced-motion?

Pass the reducedMotion option, and targets are reached at once, with no glide and no fade. prefersReducedMotion() from @n-uf/motion/browser returns a function that reads the (prefers-reduced-motion: reduce) media query, which is what the option takes. settleValue, springValue and createLayoutMotion all accept it.

Does resizing a window stretch its content?

Not when you use reveal. reveal(drawn, target) says how to draw a window gliding at drawn whose content is laid out at target: move the element, draw the frame at its animated size and clip the content. There is no scale and no layout per frame, so content is never stretched.