contents

Animations#

Everything that moves in carrot is animated: windows opening, closing, and travelling to new spots, workspace switches, the scrolling strip's view, layer surfaces like bars and launchers, even the border color as focus moves. It all ships on by default, every kind is tunable on its own, and the whole section applies live on burrow reload.

This is the block the default config ships:

animations {
    // off
    // Global speed dial; 2.0 runs everything at half speed.
    slowdown 1.0
    // Name a bezier once, use it as any ease's curve=:
    // curve "overshot" 0.05 0.9 0.1 1.05
    spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
    window-open { ease duration-ms=150 curve="ease-out-expo"; style "popin" perc=80 }
    window-close { ease duration-ms=150 curve="ease-out-quad"; style "popin" perc=80 }
    workspace-switch { spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001; style "slide" }
    border-color { ease duration-ms=200 curve="ease-out-quad" }
    // The remaining kinds: window-move, window-resize,
    // view-movement, layer-open, layer-close.
}

Kinds without their own motion inherit the section's spring; off turns every animation into an instant jump.

The kinds#

Nine node names, one per thing that moves:

KindAnimatesStyles
window-opena window mappingpopin, fade, slide
window-closea window unmappingpopin, fade, slide
window-movetiles travelling to new rects (layout changes, swaps, re-tiles)none
window-resizethe resize crossfade: old and new content mix across the size changenone
workspace-switchthe switch between workspacesslide, slidevert, fade, slidefade, slidefadevert
view-movementthe scrolling strip's view following focusnone
layer-opena layer surface mapping (bars, launchers)popin, fade, slide
layer-closea layer surface unmappingpopin, fade, slide
border-colorthe border blending between active and inactivenone

Each kind block takes off (disable just that one), one motion (a spring or an ease; declaring both in one kind is an error), and a style where the table allows one. A kind with no motion of its own inherits the section default.

window-resize deserves a note: on a size change past a few pixels, the old content is captured once, the new content keeps rendering live, and the two crossfade against each other while the geometry animates, clipped to the rounded corners. Interactive drags are exempt; while you hold a grab, windows track the pointer 1:1 and nothing chases.

Motion#

spring damping-ratio=1.0 stiffness=800 epsilon=0.0001 is physical motion. Springs carry velocity across retargets, so an animation interrupted halfway flows into its new target instead of restarting. damping-ratio (0.1..10) sets the character: 1.0 is critically damped and settles cleanly, below it bounces, above it drags. stiffness (1..100000) sets the speed. epsilon (0.00001..0.1) is the rest threshold that decides when the spring counts as done.

ease duration-ms=150 curve="ease-out-expo" is fixed-duration easing, 0..10000 ms. The built-in curves are linear, ease-out-quad, ease-out-cubic, and ease-out-expo; leaving curve off means ease-out-cubic. Any other name refers to a bezier you declared.

curve "name" x1 y1 x2 y2 declares a named cubic bezier, CSS-style. The x control points clamp into 0..1; the y points are free, so a y past 1.0 overshoots the target and settles back. Referencing a curve you never declared is a config error, as is declaring the same name twice.

Styles#

Motion says how fast something travels; a style says what the pixels do on the way.

Windows and layer surfaces take:

  • style "popin" perc=80 scales in from 80% size while fading. perc is 0..100.
  • style "fade" changes opacity only.
  • style "slide" dir="top" slides in from an edge: top, bottom, left, or right. A layer surface with no dir slides from the edge it anchors to.

Workspace switches take slide (the workspaces travel as one plane), slidevert (the same, vertically), fade, and slidefade / slidefadevert (travel and fade together, with perc= setting the fade depth). Plain slide follows the workspace axis: horizontal by default on dwindle, flipped by workspace-axis "vertical", and always vertical while any workspace runs the scrolling strip; see the axis rule.

The motion-only kinds (window-move, window-resize, view-movement, border-color) take no style; giving them one is a config error.

The clock#

Animations sample time at the predicted moment their frame reaches glass: the last flip time plus one refresh period, not the moment the frame was drawn. Every animation in a frame agrees on that instant, so nothing drifts within a frame, and motion stays smooth even when compose times wobble. On VRR outputs, where no fixed period exists, the clock falls back to real time.

slowdown stretches every animation's clock (2.0 is half speed, handy for tuning), and off short-circuits everything to an instant jump; both are honored immediately on reload.

Interruptions#

Every animation retargets rather than restarts:

  • A window whose target rect changes mid-flight folds its current offset and velocity into the new flight.
  • A workspace switch mid-slide turns around from wherever it is, carrying velocity.
  • The strip's view continues from its current position toward the new one.
  • A border color change starts from the color currently on glass, not the color it was heading for. The blend runs through OkLab, so the in-between colors stay clean.

Close animations outlive their window: carrot captures the window's last composed frame before the surface goes away and animates that, so a closed window pops out even though the client is already gone.

Per-window overrides#

Window rules can exempt or restyle individual windows:

window-rule {
    match app-id=#"^steam_app_"#
    no-anim
}
window-rule {
    match app-id=#"^foot$"# title=#"^scratch"#
    animation "slide" dir="top"
}

no-anim skips that window's open and close animations entirely. animation overrides its open and close style, same grammar as style here. Fullscreen windows skip move and close animations on their own; nothing tweens over a game.

In Lua#

Same schema through the same validators, with three mechanical differences: curves is one name-keyed map instead of repeated curve nodes, kind names are snake_case, and a style is a table with the name as its first element.

animations = {
    slowdown = 1.0,
    curves = { overshot = { 0.05, 0.9, 0.1, 1.05 } },
    spring = { damping_ratio = 1.0, stiffness = 800, epsilon = 0.0001 },
    window_open = { ease = { duration_ms = 150, curve = "ease-out-expo" }, style = { "popin", perc = 80 } },
    workspace_switch = { spring = { damping_ratio = 1.0, stiffness = 1000, epsilon = 0.0001 }, style = { "slide" } },
}

The rest of the Lua story is on Lua configuration.