A small, CSS-first motion kit: scroll reveals, staggered lists, count-ups, a ticker, scroll-linked parallax, word-by-word headings, pointer tilt, page transitions and micro-interactions. Opacity and transform only (no layout shift), never hides server-rendered content before hydration, and every primitive renders its final state under reduced motion or data-motion="off".
Saltmarsh Ferries · Winter timetable
Forty-one crossings a day, and the 07:40 still leaves on the minute.
- Daily crossings
- 41
- On time since March
- 98.6%
- Passengers a week
- 12,480
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { Reveal, Stagger, CountUp, Ticker } from "@vamos-ui/react/motion";
<Reveal effect="rise"><Card>…</Card></Reveal>
<Stagger asChild step={70}><ul>{rows}</ul></Stagger>
<CountUp value={1240} />
// A whole site: <SiteShell motion="full" | "subtle" | "off">Motion explains, it doesn't decorate. Use it where something changes or arrives — a page, a list, a number — and leave settled UI still. One expressive moment per screen is plenty.
Admin panels: fade or a 6–8 px rise at most, no parallax, no tilt. Sites: subtle by default; full for portfolio-style pages where the motion is part of the brand.
Never hide what is already on screen. Server-rendered content in view at load stays visible; reveals only start hidden for content below the fold or mounted later, so there is no flash and CLS stays 0.
Respect the visitor: prefers-reduced-motion turns every primitive into its final state, data-motion="off" on <html> stops all motion in the kit, and moving content (the Ticker) always has a pause control.
Durations follow density (compact is 20% quicker). Keep entrances under a second and exits shorter than entrances.
Examples
Reveal effects
fade, rise, blur, scale and clip. Content already on screen when the page loads stays put; content below the fold is hidden just before paint and revealed on scroll.
Opacity only. The quietest choice — tables, settings, dense panels.
Fade plus a short rise. The default for site sections.
Comes into focus. Use once per page, for an image or a quote.
Grows from 94%. Cards and tiles that sit on their own.
Wipes down from the top edge. Photography and big type.
Stagger
Children arrive in sequence. asChild keeps your list element; the step defaults to the level's --motion-stagger.
Today’s follow-ups
5 deals · €36,980- 09:30Defne ArslanKestrel Bakery€4,200
- 11:00Marcus HaleHarbor & Pine Hotels€18,750
- 13:15Ines OkaforSable Dental Clinic€2,980
- 15:40Tomas BrandtNorthwind Studio€9,600
- 17:00Yuki MoritaQuarry Lane Coffee€1,450
Count-up, per locale
Intl formatting, tabular figures and a reserved width. Screen readers hear the final value only.
- Reviews
- 1,240
- Average rating
- 4.8
- Revenue, October
- €48,250.50
English (US)
- Reviews
- 1.240
- Average rating
- 4,8
- Revenue, October
- €48.250,50
Türkçe
Ticker
Pure CSS marquee with a pause control; static and wrapped under reduced motion.
Text reveal
Word or line stagger for headings, read as one string by assistive tech.
Small rooms, slow mornings, and nothing on the walls to scroll.
Open Tuesday to Sunday, 08:00–16:00 Kitchen closes at 15:30 No bookings for under six
Tilt, magnetic and micro-interactions
Pointer effects run for mouse and trackpad only. vm-press, vm-lift, vm-underline and vm-nudge are plain classes.
Read the membership terms or house rules.
Parallax and scroll progress
animation-timeline where supported, a passive rAF fallback elsewhere.
Page transitions and skeleton swap
View Transitions when available, a fade otherwise; loaded content cross-fades over its skeleton.
November
€9,095.50
invoiced across 3 clients · 2 paid, 1 due this week
SiteShell motion levels
full, subtle (default) and off. off is also exactly what a visitor with reduced motion sees at any level.
The default: short fades and small rises, count-ups, the logo ticker.
Atlas Dining Room · 2026
A year at the long table
- Average rating
- 4.8
- Source: 1,240 reviews, Oct 2026
- Reviews
- 1,240
- Bookings via WhatsApp
- 38% percent
- Median first reply
- 11min minutes
Suppliers we cook with
API
Reveal
| Prop | Type | Default |
|---|---|---|
effectHow it enters. Opacity, transform, filter or clip-path — never layout. | "fade" | "rise" | "blur" | "scale" | "clip" | "rise" |
delayWait before starting. | number (ms) | — |
durationOverride the level's --motion-reveal. | number (ms) | — |
asChildAnimate your element instead of a wrapping div. | boolean | false |
onceReveal once and stop observing. With false it hides again when scrolled back above it. | boolean | true |
rootMarginIntersectionObserver rootMargin — when “in view” starts. | string | "0px 0px -8% 0px" |
thresholdIntersectionObserver threshold. | number | 0 |
disabledSkip the animation; the content just shows (blocks pass editMode here). | boolean | false |
onRevealCalled once the element starts revealing. | () => void | — |
Stagger
| Prop | Type | Default |
|---|---|---|
stepTime between children. The 13th child onwards shares the last slot. | number (ms) | --motion-stagger (60 / 90) |
effect · delay · duration · asChildApplied to each direct child. | as Reveal | — |
onceReveal once and stop observing. With false it hides again when scrolled back above it. | boolean | true |
rootMarginIntersectionObserver rootMargin — when “in view” starts. | string | "0px 0px -8% 0px" |
thresholdIntersectionObserver threshold. | number | 0 |
disabledSkip the animation; the content just shows (blocks pass editMode here). | boolean | false |
onRevealCalled once the element starts revealing. | () => void | — |
CountUp
| Prop | Type | Default |
|---|---|---|
valueFinal value — what the server renders and screen readers hear. | number | — |
fromStart value. | number | 0 |
durationLength of the count. | number (ms) | 1400 subtle · 1800 full |
delayWait after it becomes visible. | number (ms) | 0 |
localeBCP 47 locale for Intl.NumberFormat. | string | LocaleProvider |
formatDefault: the value's own decimals (max 2). Currency, percent, compact all work. | Intl.NumberFormatOptions | — |
Ticker
| Prop | Type | Default |
|---|---|---|
durationOne full loop. | number (s) | 40 |
directionTravel direction; mirrors in RTL. | "start" | "end" | "start" |
asElement for each copy of the items. | "div" | "ul" | "ol" | "div" |
gapSpace between items. | CSS length | clamp(2.5rem, 6cqi, 4.5rem) |
fadeFade the start and end edges. | boolean | true |
controls · pauseLabelThe pause control (a native checkbox, works without JS). | boolean · string | true · "Pause animation" |
TextReveal
| Prop | Type | Default |
|---|---|---|
childrenThe text; in line mode \n separates lines. | string | — |
asElement to render. | "h1" … "h4" | "p" | "span" | "div" | "blockquote" | "h2" |
byUnit of the stagger. | "word" | "line" | "word" |
effectmask: each unit rises from behind its baseline. | "mask" | "fade" | "blur" | "mask" |
step · delayTime between units; wait before the first. | number (ms) | 45 words · 110 lines |
Parallax
| Prop | Type | Default |
|---|---|---|
shiftTravel each way while crossing the viewport. Negative moves faster than the page. | number (px) | 40 |
clipClip to the frame and over-size the layer (give the frame an aspect ratio or height). | boolean | true |
ScrollProgress
| Prop | Type | Default |
|---|---|---|
fixedFixed to the top of the viewport; false keeps it in the flow. | boolean | true |
Tilt
| Prop | Type | Default |
|---|---|---|
maxMaximum lean. | number (deg) | 5 |
glareA soft light that follows the pointer. | boolean | false |
levelLowest motion level that tilts. | "subtle" | "full" | "subtle" |
asChildTilt the child itself. | boolean | false |
Magnetic
| Prop | Type | Default |
|---|---|---|
strengthShare of the pointer offset it follows. | number (0–1) | 0.3 |
limitCap on the travel. | number (px) | 10 |
levelLowest motion level that pulls. | "subtle" | "full" | "full" |
asChildPull the child itself (its own transition list is replaced while it moves). | boolean | false |
PageTransition · startPageTransition
| Prop | Type | Default |
|---|---|---|
pageKeyPageTransition: fades the content in on every change after the first render. | string | — |
startPageTransition(update, { scope })Runs the update in a View Transition (only scope cross-fades) when supported and motion is allowed. | => "view-transition" | "none" | — |
SkeletonSwap
| Prop | Type | Default |
|---|---|---|
loadingSkeleton while true; content cross-fades in when it turns false. aria-busy while loading. | boolean | — |
skeletonThe placeholder — give it the content's size. | ReactNode | — |
MotionScope · motionLevel · useReveal · useScrollShrink
| Prop | Type | Default |
|---|---|---|
MotionScope levelSets data-motion for a subtree (SiteShell does it for a site). | "full" | "subtle" | "off" | — |
motionLevel(el)The level that applies to an element, reduced motion included. | => "full" | "subtle" | "off" | — |
useReveal(ref, options)The reveal state machine for your own element (data-reveal + the returned props). | => initial props | — |
useScrollShrink(ref, { threshold })Sets data-scrolled on a sticky header; pair with the vm-shrink class. | => boolean | — |
CSS (styles/motion.css)
| Prop | Type | Default |
|---|---|---|
--motion-fast · base · slow · revealDuration scale; full stretches reveals to 960 ms. Multiplied by --motion-k (compact 0.8, spacious 1.12). | time | 140 · 240 · 420 · 680 ms |
--ease-out-quint · --ease-out-expo · --ease-in-out · --ease-springThe kit's curves; --ease-spring is a damped linear() spring (~4% overshoot). | easing | — |
vm-press · vm-lift · vm-underline · vm-nudgePress scale, hover lift, drawn underline, nudging trailing icon. | class | — |
vm-enter (+ --vm-i)CSS-only entrance from the first paint — for above-the-fold copy (the Hero uses it). | class | — |
vm-expand · details.vm-detailsAnimated height: grid-rows for data-state panels, ::details-content for native details. | class | — |
vm-shrinkSticky header that tightens on scroll with transforms only. | class | — |