Keyframes
Write a @keyframes block next to the component that animates with it, and get its name back from keyframes().
A keyframe used by one component does not have to live in the config. Write it where it is used:
import { css, keyframes } from '../styled-system/css'
const fadeIn = keyframes({
from: { opacity: 0 },
to: { opacity: 1 },
})
// → 'kf_feVUdh'
const toast = css({ animation: `${fadeIn} 0.2s ease-out` })
keyframes() returns the name of the block, and the stylesheet gets the block:
@layer tokens {
@keyframes kf_feVUdh {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
}
The name works anywhere a keyframe name does: animation, animationName, a template literal, or an inline style.
Stops
A stop is from, to or a percentage, and its declarations are written as in a style object, tokens included:
const pulse = keyframes({
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.5, color: 'red.300' },
})
color: 'red.300' is written var(--colors-red-300), as in css(). Conditions do not go in a stop: @keyframes holds
no selectors or media queries. Put the condition on the animation instead:
css({ animation: `${pulse} 2s infinite`, _motionReduce: { animation: 'none' } })
The name is a hash of the stops
The same stops anywhere in the project are one block under one name, so a keyframe can be declared in each component that needs it without repeating the CSS.
The order the stops were written in is part of the hash, so { from, to } and { to, from } are two blocks. Order can
change an animation: a stop written twice, as from and 0%, takes the later one's value where the two overlap.
With a prefix, the name starts with it: app-kf_feVUdh.
Sharing keyframes
A keyframe is a value, so share it as one:
animations.ts
import { keyframes } from '../styled-system/css'
export const fadeIn = keyframes({ from: { opacity: 0 }, to: { opacity: 1 } })toast.ts
import { css } from '../styled-system/css'
import { fadeIn } from './animations'
export const toast = css({ animation: `${fadeIn} 0.2s ease-out` })The block is written from the module that calls keyframes(), so that module has to be one the build reads: inside
include.
Or in the config
A keyframe the whole design system shares, used by name across the app, still belongs in theme.keyframes:
export default defineConfig({
theme: {
extend: {
keyframes: {
spin: { to: { transform: 'rotate(360deg)' } },
},
},
},
})
The two differ in what happens to an unused one. A theme keyframe nothing animates is pruned with
prune.keyframes. A keyframes() block is written while the call exists, whether or
not anything in the stylesheet names it, because its name can be read where the build cannot see: an inline style, or
a string built at runtime.
Limitations
-
The stops have to be known at build time. The call is replaced by its name when the module is compiled, and the name is a hash of the stops, so a stop the build cannot read leaves it with no name to write:
// ❌ `to` is a parameter, so the build cannot name this block. const fade = (to: number) => keyframes({ from: { opacity: 0 }, to: { opacity: to } })The build fails on the call rather than ship it. Vary what the stops read instead: a custom property set where the animation runs,
to: { opacity: 'var(--fade-to)' }, or the animation's own duration, delay and direction. -
Compiled by Vite. Without the Vite plugin,
keyframes()throws when called, ascss()does.