cardstock

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 (with effect="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;
  }
}

On this page