Search the docs

Search Keyframery's docs

Keyframery

Speed, easing and cuts

Tune shadcn/ui animations like colours: CSS variables set speed and easing, <Cuts> props pick each component's cut, and attributes change one element.

There are three levels. Use the first one that does what you need.

You want toUse
change speed or easing everywhere, or in one sectionCSS variables
pick a different cut for dialogs, sheets, tabs or toasts<Cuts> props
change one element, or turn motion off in one areaattributes

The Theme page builds the variables and props for you, with a live preview.

CSS variables

VariableDefaultWhat it does
--kf-pace1multiplies every duration (2 is half speed)
--kf-easecubic-bezier(0.22, 1, 0.36, 1)easing for entrances
--kf-ease-exitcubic-bezier(0.5, 0, 0.75, 0)easing for exits
--kf-travel0.35how far toward the opener a dialog starts (0 starts from the centre)
--kf-blur8pxthe blur a dialog starts from
--kf-depth0.94the scale a dialog starts from
--kf-hold300mshow long LoadCut waits before showing a skeleton

For the whole app, put them in your global stylesheet:

app/globals.css
:root {
  --kf-pace: 1.2;
  --kf-blur: 4px;
}

Keyframery declares its defaults with zero specificity, so your :root always wins, wherever it sits in the file.

For one section, set them on any element. They cascade, and dialogs and menus opened from inside the section use them too:

<section style={{ "--kf-pace": 1.5 } as React.CSSProperties}>…</section>

<Cuts> props

<Cuts dialog="punch-in" sheet="slide" tabs="whip" pace={1.2} />

Prop

Type

none means "leave the library's own motion alone".

What each cut does

Durations are multiplied by --kf-pace.

CutOpeningClosing
rack-focusstarts 35% of the way toward the button that opened it, at 0.94 scale and 8px blur, then sharpens in place over 340 ms while the backdrop fades inreturns toward the button over 220 ms
punch-infrom the centre: scale 0.85, past 1.02, to 1, over 260 msscales to 0.96 and fades over 160 ms
fadeopacity only, 200 ms150 ms
slide-sinkthe panel travels in from its edge over 460 ms, while the page behind scales to 0.975, dims slightly and rounds its corners260 ms, and the page returns
slidelike slide-sink, without the page stepping backsame
j-cutthe tab pill whips to the new tab, the old panel leaves, and the new one enters 70 ms later while the frame's height morphs
whipno lead, 160 ms, with a 4px motion blur on both panels
tabs fadea 180 ms crossfade with the height morph
cut-on-actionthe toast flies from the element you pressed, from 0.4 scale and a 6px blur, over 480 msthe library's own exit

The eleven tuned components have no menu. They follow the variables.

One element or one area

AttributePut it onEffect
data-cut="<cut>"a shadcn part, for example <DialogContent data-cut="fade">use that cut here
data-cut="none"a part, or any ancestor like <section>stock shadcn motion inside
data-cut-pace="1.5"a part or any ancestorsets --kf-pace there
data-cut-origin="center"a dialog's contentopen from the centre, not from the button
<DialogContent data-cut="punch-in">…</DialogContent>

<section data-cut="none">
  {/* everything in here, including the dialogs it opens, uses shadcn's own motion */}
</section>

<Tabs data-cut="whip">…</Tabs>

For tabs, put data-cut on the Tabs root. The four components under Animate your app take a cut prop instead, for example <ListCut cut="none">, and they also respect data-cut="none" and data-cut-pace on their ancestors.

Which setting wins

From weakest to strongest:

  1. Keyframery's default cut
  2. the <Cuts> prop for that group
  3. the element's own data-cut

The order of your stylesheets doesn't matter. A named cut, such as data-cut="fade", applies only to the element it's written on. Only data-cut="none" reaches the elements inside, including dialogs that render in a portal. See Portals.

Turn it off

<Cuts enabled={false} />

Every component plays shadcn's own motion. To turn it off in one area only, put data-cut="none" on it.

Listen to cuts

Every cut fires a keyframery:cut event on document:

document.addEventListener("keyframery:cut", (e) => {
  const { cut, component, phase, ms } = (e as CustomEvent).detail
  // e.g. { cut: "rack-focus", component: "dialog", phase: "enter", ms: 340 }
})

It's handy for analytics and in tests. See Turning motion off in tests.

Questions

How do I slow down or speed up every animation?

Set --kf-pace in your global CSS, for example :root { --kf-pace: 1.2; }. It multiplies every duration, so 2 is half speed. <Cuts pace={1.2} /> does the same.

How do I turn the animation off for one dialog?

Add data-cut="none" to its DialogContent, and that dialog keeps shadcn's own motion. On a section, data-cut="none" covers everything inside it, including the dialogs it opens.

Can a dialog open from the centre instead of the button?

Yes. Add data-cut-origin="center" to one dialog's content, or use <Cuts dialog="punch-in" /> for every dialog.

On this page