Animation
Built-in motion where it earns its keep; CSS, Motion, or nothing for the rest.
cardstock animates a few things itself, each one you can turn off and do your own way:
- the flip, with a sheen (and optionally a lift, or a turn toward the press):
Card.Body effect; - the tilt:
Card.TiltSurface; - the carousel: its settling spring, which carries the speed of your finger, and its coverflow;
- the number scramble, which changes text.
Everything else is state, and moving it is up to you.
CSS
Most cards need nothing else. Transition the variables and attributes:
- the flip on
--card-flipped(witheffect="none"on the body), or on the per-frame--card-flip-*; - the tilt on
--card-tilt-*; - the frozen overlay on
data-starting-style/data-ending-style; - the meter on
--card-spending-ratio.
Keep UI motion short. A reveal or a small state change should stay under 300ms and ease out. The freeze is the exception, because it's a moment meant to be watched.
Motion
Pass a motion element to render, and read the state it animates on from useCard():
function Body({ children }) {
const { flipped } = useCard();
return (
<Card.Body
effect="none"
render={<motion.div animate={{ rotateY: flipped ? 180 : 0 }} transition={{ type: "spring" }} />}
>
{children}
</Card.Body>
);
}For per-frame values, subscribe instead of re-rendering. useCard().reveal, useCard().freeze and useCard().flip are progress objects with get() and subscribe(fn), and useCarouselPosition() works the same way.
Performance
What moves every frame is a CSS variable the library writes without re-rendering: the carousel's position, a slide's offset, the tilt, the flip. The carousel's are registered as non-inheriting numbers, so a swipe restyles only the slides, not every card on the track (see reading them deeper down). If you animate something of your own on a variable, set it on the element that moves, not on a wrapper around many.
Reduced motion
The parts respect prefers-reduced-motion:
- tilt switches off;
- the flip cross-fades the faces instead of turning;
- the number reveals without a scramble;
- the carousel settles with a 200ms ease instead of a spring, and its neighbours stop turning and receding (they still fade).
In your CSS, swap movement for a short fade rather than removing everything. For example, cross-fade the faces instead of turning the card:
@media (prefers-reduced-motion: reduce) {
[data-slot="card-body"],
[data-slot="card-back"] {
transform: none;
}
[data-slot="card-front"],
[data-slot="card-back"] {
transition: opacity 200ms;
}
[data-slot^="card-"]:not([data-visible])[data-side] {
opacity: 0;
}
}