ComponentsFoundations

Motion

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".

IntersectionObserver, CSS scroll-driven animations, View Transitions API motion/core.ts, motion/reveal.tsx, motion/count-up.tsx, motion/ticker.tsx, motion/parallax.tsx, motion/text-reveal.tsx, motion/pointer.tsx, motion/page-transition.tsx, motion/skeleton-swap.tsx, motion/scroll-shrink.ts, motion/motion-scope.tsx, styles/motion.csstheme · material · accent · density · font

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
  • Harbour Quay → North Pier
  • Eastbank → Isle of Wren
  • North Pier → Saltmarsh
  • Isle of Wren → Harbour Quay
  • Saltmarsh → Eastbank

Installation

$ pnpm add @vamos-ui/react

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.

fade

Opacity only. The quietest choice — tables, settings, dense panels.

rise

Fade plus a short rise. The default for site sections.

blur

Comes into focus. Use once per page, for an image or a quote.

scale

Grows from 94%. Cards and tiles that sit on their own.

clip

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.

English (US)

Reviews
1,240
Average rating
4.8
Revenue, October
€48,250.50

Türkçe

Reviews
1.240
Average rating
4,8
Revenue, October
€48.250,50

Ticker

Pure CSS marquee with a pause control; static and wrapped under reduced motion.

  • Kestrel Bakery
  • Harbor & Pine
  • Northwind Studio
  • Sable Clinic
  • Quarry Lane Coffee
  • Wren Island Ferries
  • Atlas Bindery
  • Websites
  • Booking
  • Menus
  • Reviews
  • Payments
  • Newsletters

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.

Northwind StudioMember
Ines OkaforSince 2019 · #0412

Read the membership terms or house rules.

Parallax and scroll progress

animation-timeline where supported, a passive rAF fallback elsewhere.

A long table set for dinner under warm pendant lights
The long table · Atlas Dining Room

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
11 minutes

Suppliers we cook with

API

Reveal

PropTypeDefault
effect
How it enters. Opacity, transform, filter or clip-path — never layout.
"fade" | "rise" | "blur" | "scale" | "clip""rise"
delay
Wait before starting.
number (ms)—
duration
Override the level's --motion-reveal.
number (ms)—
asChild
Animate your element instead of a wrapping div.
booleanfalse
once
Reveal once and stop observing. With false it hides again when scrolled back above it.
booleantrue
rootMargin
IntersectionObserver rootMargin — when “in view” starts.
string"0px 0px -8% 0px"
threshold
IntersectionObserver threshold.
number0
disabled
Skip the animation; the content just shows (blocks pass editMode here).
booleanfalse
onReveal
Called once the element starts revealing.
() => void—

Stagger

PropTypeDefault
step
Time between children. The 13th child onwards shares the last slot.
number (ms)--motion-stagger (60 / 90)
effect · delay · duration · asChild
Applied to each direct child.
as Reveal—
once
Reveal once and stop observing. With false it hides again when scrolled back above it.
booleantrue
rootMargin
IntersectionObserver rootMargin — when “in view” starts.
string"0px 0px -8% 0px"
threshold
IntersectionObserver threshold.
number0
disabled
Skip the animation; the content just shows (blocks pass editMode here).
booleanfalse
onReveal
Called once the element starts revealing.
() => void—

CountUp

PropTypeDefault
value
Final value — what the server renders and screen readers hear.
number—
from
Start value.
number0
duration
Length of the count.
number (ms)1400 subtle · 1800 full
delay
Wait after it becomes visible.
number (ms)0
locale
BCP 47 locale for Intl.NumberFormat.
stringLocaleProvider
format
Default: the value's own decimals (max 2). Currency, percent, compact all work.
Intl.NumberFormatOptions—

Ticker

PropTypeDefault
duration
One full loop.
number (s)40
direction
Travel direction; mirrors in RTL.
"start" | "end""start"
as
Element for each copy of the items.
"div" | "ul" | "ol""div"
gap
Space between items.
CSS lengthclamp(2.5rem, 6cqi, 4.5rem)
fade
Fade the start and end edges.
booleantrue
controls · pauseLabel
The pause control (a native checkbox, works without JS).
boolean · stringtrue · "Pause animation"

TextReveal

PropTypeDefault
children
The text; in line mode \n separates lines.
string—
as
Element to render.
"h1" … "h4" | "p" | "span" | "div" | "blockquote""h2"
by
Unit of the stagger.
"word" | "line""word"
effect
mask: each unit rises from behind its baseline.
"mask" | "fade" | "blur""mask"
step · delay
Time between units; wait before the first.
number (ms)45 words · 110 lines

Parallax

PropTypeDefault
shift
Travel each way while crossing the viewport. Negative moves faster than the page.
number (px)40
clip
Clip to the frame and over-size the layer (give the frame an aspect ratio or height).
booleantrue

ScrollProgress

PropTypeDefault
fixed
Fixed to the top of the viewport; false keeps it in the flow.
booleantrue

Tilt

PropTypeDefault
max
Maximum lean.
number (deg)5
glare
A soft light that follows the pointer.
booleanfalse
level
Lowest motion level that tilts.
"subtle" | "full""subtle"
asChild
Tilt the child itself.
booleanfalse

Magnetic

PropTypeDefault
strength
Share of the pointer offset it follows.
number (0–1)0.3
limit
Cap on the travel.
number (px)10
level
Lowest motion level that pulls.
"subtle" | "full""full"
asChild
Pull the child itself (its own transition list is replaced while it moves).
booleanfalse

PageTransition · startPageTransition

PropTypeDefault
pageKey
PageTransition: 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

PropTypeDefault
loading
Skeleton while true; content cross-fades in when it turns false. aria-busy while loading.
boolean—
skeleton
The placeholder — give it the content's size.
ReactNode—

MotionScope · motionLevel · useReveal · useScrollShrink

PropTypeDefault
MotionScope level
Sets 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)

PropTypeDefault
--motion-fast · base · slow · reveal
Duration scale; full stretches reveals to 960 ms. Multiplied by --motion-k (compact 0.8, spacious 1.12).
time140 · 240 · 420 · 680 ms
--ease-out-quint · --ease-out-expo · --ease-in-out · --ease-spring
The kit's curves; --ease-spring is a damped linear() spring (~4% overshoot).
easing—
vm-press · vm-lift · vm-underline · vm-nudge
Press 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-details
Animated height: grid-rows for data-state panels, ::details-content for native details.
class—
vm-shrink
Sticky header that tightens on scroll with transforms only.
class—