SolutionsPackagesComponentsCLI — Coming soonResourcesDocumentationStorybookChangelogBlogAboutSign up freeGet early access

Documentation

Overview

Token-driven timelines, staggered sequences, and awaitable enter/exit animations with @ds-gn/motion.

@ds-gn/motion gives DS-GN projects timelines, sequencing, and enter/exit motion on the Web Animations API. Durations and easings come from the motion tokens, every call respects prefers-reduced-motion, and the core has no framework dependency, so it works in Lit, React, and plain pages alike.

npm install @ds-gn/motion

Importing either entry point never touches window or document, so it is safe in server-rendered code. Motion only runs when you call it in the browser.

What it does

PageFor
TimelineOrdered steps that start before the previous one ends
SequenceOne preset across many elements, or letter by letter
Enter and exitAwaitable entrances and exits, and viewport triggers
LitMotionController, scoped to a Lit host

The rest of this page covers where to use motion and what every call shares: presets, tokens, reduced motion, and the handle that timelines and sequences return.

Where to use motion

Motion runs in JavaScript, so it starts once your scripts load, and on a server-rendered page that is after the content has already painted. Use it where that does not matter:

  • Below the fold. Start entrances with an in-view trigger (inView or the Lit controller’s inViewSequence), so they play as each section scrolls into view rather than on load. If a section is already on screen when your scripts load, leave it static rather than hide it and animate it back. If it is off screen, you can hide its parts from script right then, so a jump to it (an in-page link) shows no unanimated frame before the entrance starts.
  • Minor elements above the fold, such as an eyebrow or a badge, and never the page’s largest text or image. Largest Contentful Paint does not count an element while it is at opacity 0, so animating it in moves the metric to whenever the animation reveals it.

DS-GN components that animate expose a motion attribute with values of their own: sections such as Hero and FeatureTabs take motion="on" or "off" (default "off"), and Modal takes the direction it animates from.

Keep everything else above the fold static. If a minor above-the-fold element must stay hidden until its entrance, hide it only until its custom element upgrades (for example with :not(:defined) in your stylesheet), only when the user has not asked for reduced motion, and reveal it under <noscript>, so it cannot stay hidden when JavaScript is off. A script that fails to load leaves such an element hidden, so reserve the pattern for decoration.

Presets

PresetEnterExit
slideUpy 12px → 0, opacity 0 → 1y 0 → 8px, opacity 1 → 0
fadeopacity 0 → 1opacity 1 → 0
scaleInscale 0.96 → 1, opacity 0 → 1scale 1 → 0.96, opacity 1 → 0

The slideInLeft, slideInRight, slideInTop, and slideInBottom presets move a sheet from/to its full width or height (translateX/Y(±100%)) without fading. Under reduced motion they substitute a fade, so no empty keyframes or spatial movement remain. Preset names describe the entrance; each includes its matching exit, so exit(panel, 'slideInLeft') slides back to the left edge, just as exit(panel, 'scaleIn') uses the scale preset’s exit.

Only transform and opacity animate, so presets stay on the compositor.

Every call takes a y option, in pixels, that replaces a preset’s movement with vertical travel: an entrance starts y below its resting place (negative values start above), and an exit ends there. enter(el, 'fade', { y: 4 }) fades in with a small rise. Like all movement, y is dropped under reduced motion.

An entrance ends on the element’s own resting style. Make sure that style is visible: an element hidden with opacity: 0 in CSS fades in and then disappears again. Below the fold, leave the element visible in CSS instead of hiding it until its trigger: an entrance holds its first frame from the moment it starts, and an in-view trigger fires as soon as the element reaches the viewport, so at most a sliver at the edge shows before the entrance takes over. Hiding it in CSS would keep it hidden for anyone whose scripts never run.

Tokens

Durations and easings are named after the motion tokens and resolve from custom properties on the animated element when the animation starts:

NameCustom propertyFallback
'quick'--duration-quick180ms
'moderate'--duration-moderate240ms
'emphasized'--ease-emphasizedcubic-bezier(0.16, 1, 0.3, 1)
'standard'--ease-standardcubic-bezier(0.2, 0, 0, 1)

So a token override from @ds-gn/tokens, including one set on a single section, changes motion with no code changes. Raw milliseconds and any CSS easing work too: duration: 320, easing: 'linear'.

Reduced motion

When prefers-reduced-motion: reduce is set:

  • translate and scale are dropped;
  • delays, staggers, and overlaps collapse to 0;
  • everything cross-fades at duration.quick, or, with reducedMotion: 'jump', goes straight to the end state;
  • promises still resolve, so code awaiting an exit never hangs.

prefersReducedMotion() is exported for timer-driven animation that needs the same gate.

Playback

timeline and sequence return a Playback:

MemberDescription
finishedPromise that resolves when everything finishes or is cancelled
durationTotal scheduled length in ms
finish()Jump every animation to its end state
cancel()Remove the animations’ effects; elements return to their own CSS

finished never rejects.

© 2026 LightLeap Labs LLC. All rights reserved. · Terms of Service · Privacy Policy · Cookies · Contact · Support