Browse Learn

Motion

Make interactions feel responsive and considered. Use subtle shared motion to explain changes, with immediate feedback and reduced-motion alternatives.

Try the essentials

These previews run only when requested. With reduced motion enabled, they stay still and fully visible.

A shared rhythm

Local token overrides change a component without changing the rest of the interface. Prefer short opacity and transform effects. Keep layout, navigation, and user feedback immediate.

:root {
  --motion-normal: 300ms;
  --motion-ease: ease-in-out;
  --motion-distance: 8px;
}

/* Override tokens at the app root to keep components consistent. */

Use the shared 300ms duration for hover colors, palette changes, icon fades, and entrance effects. Keep the same timing and easing for the same interaction across components. Document any intentional exception.

Palette and theme changes interpolate the shared color tokens over the normal duration, so surfaces, borders, text, and decorative colors change together. Reduced motion disables this interpolation. Browsers without registered custom property support apply the palette immediately.

Component defaults

Buttons, links, and fields share color transitions. Disclosures animate expansion where supported and rotate their indicator. Tooltips fade in; dialogs use a subtle scale and a fading backdrop. All use the shared 300ms timing and easing, including downloaded kits. Dismissal and focus remain immediate. Static cards, badges, tables, charts, and sections stay still by default.

Reduced motion disables transitions, animations, and smooth scrolling, including pseudo-elements and backdrops. Set data-motion="none" on a component or container for the same CSS behavior without changing system preferences.

Start with CSS

The foundation includes pressed background states without button scaling, plus native dialog entrance effects. Apply an entrance class when inserting new content; nothing starts hidden in the base styles.

<article class="panel motion-enter" data-effect="slide">
  New project created.
</article>

Presets: fade (default), slide, and scale. Optional --motion-delay controls an entrance delay. Avoid delaying important content or adding entrance effects to the page’s main headline.

Bring in more only when needed

The optional motion module wraps the browser’s Web Animations API. It returns an Animation, or null when motion is disabled. The caller owns the final state, so an interrupted effect cannot leave content hidden.

import { motion, stagger } from '/js/motion.js';

motion(panel, 'slide', { duration: 280 });
stagger(items, 'fade', { step: 40 });

// A custom effect, using the same preference handling.
const animation = motion(panel, [
  { opacity: 0, transform: 'translateX(6px)' },
  { opacity: 1, transform: 'translateX(0)' }
], { duration: 240 });

// Cancellation rejects finished; handle it when sequencing.
await animation?.finished.catch(() => {});

Repeated calls replace the active effect on that element. Stagger delays are capped at 200ms. One iteration and no retained fill are enforced. Turning on reduced motion cancels active effects. Advanced keyframes must finish at the component’s natural appearance; visual review remains necessary.

Stagger without a long wait

Plan
Build
Refine

Accessible by construction

  • Honor prefers-reduced-motion; never require movement to understand a state.
  • Set data-motion="none" on a container to disable CSS effects and skip new module animations inside it.
  • Keep focus, labels, live feedback, and dialog dismissal independent of animation completion.
  • Avoid looping, parallax, scroll interception, and movement of essential content during page load.
  • For new effects, document their purpose, duration, final state, and reduced-motion fallback.

The production packager versions HTML asset references. If your own module imports motion.js, version that module dependency with your project’s deployment setup; this tool does not rewrite JavaScript imports.

Reveal sections on scroll

Use optional section entrances to introduce a new part of a marketing page. Add data-reveal="slide" or data-reveal="fade" and load the motion module once. Sections already in view stay still; sections below the viewport animate once as they enter.

<section data-reveal="slide">
  <h2>Make it your own</h2>
  <p>Choose the pieces for your next idea.</p>
</section>
<script type="module" src="/js/motion.706eaccd09b35e86.js"></script>

Content is always visible in the underlying HTML. Reduced motion, data-motion="none", missing browser support, and unavailable JavaScript keep it static. Entrances use the shared duration and distance, do not intercept scrolling, and skip sections containing keyboard focus. Use sparingly on section wrappers; avoid animating every card or essential control.

For dynamically added sections, call revealOnScroll(elements). It returns a cleanup function that disconnects that observer.