Animate entrances and hover feedback with CSS utilities
Token-driven entrance, stagger, hover and marquee utilities that switch off under reduced motion.
At a glance
- Who: Developers adding calm entrance and hover motion to kit pages.
- What: Token-driven entrance, stagger, hover and marquee utilities that switch off under reduced motion.
- Use when: For first-paint entrances, staggered lists, card lift and button press feedback.
- Avoid when: For gesture or layout animation (drag, shared-element transitions) — use a motion library instead.
- See also: Theme an admin app (dark/light included), Group content in a card section
Preview
@zedtech/zedkit-ui/styles.css (utilities animate-fade-up, animate-fade-in, animate-pop-in, stagger-*, hover-lift, hover-press, marquee-track; attributes data-reveal, data-stagger)
Children of data-stagger enter one after another.
Reduced motion shows everything at once.
Install
pnpm add @zedtech/zedkit-uiLatest: 0.4.0 (via changesets).
Example
import "@zedtech/zedkit-ui/styles.css"; // motion utilities ship with the theme
<Card className="animate-fade-up stagger-1">First</Card>
<Card className="animate-fade-up stagger-2">Second</Card>Wraps: tailwindcss (v4 @utility and @theme)
Tokens that drive motion
Every utility reads CSS custom properties with a fallback, so a page or a future identity can change how things move without touching a component.
| Token | Default | Used by |
|---|---|---|
--zk-reveal-duration | 300ms | animate-fade-up, animate-fade-in, animate-pop-in, data-reveal, data-stagger |
--zk-reveal-distance | 0.75rem | upward travel of fade-up and reveal |
--zk-reveal-ease | cubic-bezier(0.22, 1, 0.36, 1) | entrance easing |
--zk-stagger-step | 70ms | stagger-*, data-reveal-delay, data-stagger children |
--zk-lift-duration | 200ms | hover-lift, hover-press |
--zk-lift-translate | 0 -2px | hover-lift offset |
--zk-press-translate | 0 1px | hover-press offset |
--zk-marquee-duration | 40s | marquee-track loop |
:root {
--zk-reveal-duration: 400ms; /* slower entrances across the app */
--zk-stagger-step: 90ms;
}Reveal on scroll
data-reveal and data-stagger are plain attributes: the kit styles them,
your code decides when content is visible. Render "pending" (hidden), then
switch to "in" once the element enters the viewport.
"use client";
import { useEffect, useId, useState } from "react";
import { Stack, Text } from "@zedtech/zedkit-ui";
export function RevealList() {
const id = useId();
const [state, setState] = useState<"pending" | "in">("pending");
useEffect(() => {
const node = document.getElementById(id);
if (!node) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry?.isIntersecting) {
setState("in");
observer.disconnect();
}
});
observer.observe(node);
return () => observer.disconnect();
}, [id]);
return (
<Stack id={id} data-stagger={state}>
<Text>One</Text>
<Text>Two</Text>
</Stack>
);
}Variants: data-reveal-variant="fade" or "pop" on a data-reveal element;
data-reveal-delay="1" to "6" offsets it by that many stagger steps.
Marquee
marquee-track scrolls its children in a loop. Render the content twice in
the track so the -50% translate wraps seamlessly, and keep the duplicate out
of the accessibility tree with aria-hidden.