Motion

Motion tokens set how long a transition takes and how it speeds up and slows down. Use them for every transition and animation so interface changes feel consistent and can be switched off for people who ask for less motion.

  • Beta
  • Accessibility
  • Figma Code-only
  • packages/tokens/src/code/motion.json

All seven motion tokens are code-only (src/code/motion.json): Figma variables can't hold durations or easing curves, so Figma has no motion spec. Beta because the values are a proposal drawn from existing Nuton code (token-architecture §2, Motion) awaiting DS Owner sign-off, the planned read-only “Code tokens” reference page in Figma doesn't exist yet, and components only use the fast duration and the standard curve so far.

Overview

Motion shows cause and effect: a menu grows from its trigger, a selected option fills in, a new lesson slides into a list. Nuton keeps it short and quiet, so it explains a change without slowing anyone down.

Pick a duration and an easing curve, then play the transition. The dot shows the curve's shape over time; the lesson card shows the same timing on real content.

New lesson ready
  • PhotosynthesisBiology 101 · 12 min

ts
import { tokens } from '@nuton/tokens'

const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const timing: KeyframeAnimationOptions = {
  duration: reduce ? 0 : parseFloat(String(tokens['duration-base'].resolved)),
  easing: String(tokens['ease-standard'].resolved),
  fill: 'both',
}
const lift = String(tokens['space-2'].resolved)

card.animate([{ opacity: 0, transform: `translateY(${lift})` }, { opacity: 1, transform: 'translateY(0)' }], timing)
  • Durations scale with the size of the change. Small state changes use --nu-duration-fast; larger surfaces step up.
  • Easing gives motion its character. --nu-ease-standard starts quickly and settles gently, and it is the default.
  • Reduced motion is honoured everywhere: when it's on, the change still happens, just without movement.

Tokens

Generated from @nuton/tokens. Every value below is read from the package at build time.

Duration

Duration tokens
PreviewTokenValueSource
--nu-duration-instant100msCode-only
--nu-duration-fast160msCode-only
--nu-duration-base200msCode-only
--nu-duration-slow300msCode-only
--nu-duration-panel360msCode-only
--nu-duration-slower500msCode-only

Each preview uses its duration with --nu-ease-standard. Previews stop when reduced motion is on.

TokenUse it forUsed by
--nu-duration-instantPress feedback, and small overlays leaving: a tooltip, a menu or a popover closing. Exits are quicker than enters so the interface never makes people wait.Tooltip, Popover, Menu, Audio player
--nu-duration-fastHover, focus and selection changes; menus and popovers opening; toggles.Button, Link button, Button group, inputs, menus, combobox, command menu
--nu-duration-baseSmall elements entering or leaving: a toast, an inline message, a card added to a list; a scrim fading; a sheet or dialog leaving.Learner sheets and dialogs, the mobile app's scrim, Audio player
--nu-duration-slowSheets and dialogs entering; larger surfaces moving: the tutor panel sheet or navigation drawer at smaller breakpoints (D23).Learner sheets and dialogs, the mobile app's sheets, Audio player
--nu-duration-panelLarge panels and drawers growing or settling: the tutor panel, an expanding player.Learner panels, Ask Nuri
--nu-duration-slowerLarge reveals that need time to read, and one-time celebrations. Rare in the product.The mobile app's podcast player, celebrations

Easing

Easing tokens
PreviewTokenValueSource
--nu-ease-standardcubic-bezier(0.16, 1, 0.3, 1)Code-only
--nu-ease-emphasizedcubic-bezier(0.32, 0.72, 0, 1)Code-only
--nu-ease-linearcubic-bezier(0, 0, 1, 1)Code-only
--nu-ease-exitcubic-bezier(0.4, 0, 1, 1)Code-only

Each preview uses its curve over --nu-duration-slower, so the shape is easy to see. --nu-ease-linear is emitted as a cubic-bezier that behaves exactly like CSS linear.

TokenUse it forUsed by
--nu-ease-standardThe default. Fast start, long gentle stop: right for state changes and for anything entering the screen.Every component transition
--nu-ease-emphasizedLarger, more expressive moves such as a sheet or drawer sliding in, where the motion should feel deliberate.No component yet
--nu-ease-exitAnything leaving: it starts moving at once and speeds away, so a closing sheet or dialog feels responsive. Pair it with a shorter duration than the enter.Learner sheets and dialogs
--nu-ease-linearContinuous or looping motion only: loading shimmer, progress that tracks real time.No component yet (the Button spinner uses a hard-coded linear loop)

Source: packages/tokens/src/code/motion.json, flagged nuton.codeOnly. Values were collected from nuton-teach (fade-in and slide-up) and the marketing site's most-used curves; a third marketing curve close to --nu-ease-standard is to be merged into it.

Usage

Always pair a duration token with an easing token, and turn the motion off under prefers-reduced-motion.

css
/* After: import '@nuton/tokens/tokens.css' */
.lesson-card {
  transition:
    background-color var(--nu-duration-fast) var(--nu-ease-standard),
    box-shadow var(--nu-duration-fast) var(--nu-ease-standard);
}

.tutor-sheet {
  transition: transform var(--nu-duration-slow) var(--nu-ease-emphasized);
}

@media (prefers-reduced-motion: reduce) {
  .lesson-card, .tutor-sheet { transition: none; }
}

transition-colors and motion-reduce: are Tailwind's own utilities. The Nuton names come from tailwind-v4-theme.css (easing only) and the v3 preset's transitionDuration and transitionTimingFunction.

Choosing a token

  • Start with --nu-duration-fast and --nu-ease-standard. Step up only when the element is bigger or travels further.
  • Animate opacity, transform and colours. Avoid animating size or layout, which is slower and makes content jump.
  • Keep interface feedback at or below --nu-duration-base, so it never delays the next click.
  • Use --nu-ease-linear only for loops and progress; it looks mechanical on things that start and stop.
  • Never hard-code a duration or curve. If no token fits, raise it with the DS Owner.

In use

Every @nuton/ui transition uses --nu-duration-fast with --nu-ease-standard. Hover, focus and open these to see it.

Button hover

Background, border and focus shadow change together. Point at a button, or tab to it.

The menu fades in and scales up slightly from its trigger. Inside it, the switch thumb slides with the same timing.

Selection and focus

Selecting a range fills the item in; focusing a field changes its border and shadow. Both use the fast duration.

Showing study time for this week.

For example, Biology 101.

Motion in each component

ComponentWhat movesTokensWith reduced motion
ButtonBackground, border, focus shadow--nu-duration-fast · --nu-ease-standardNo transition. The loading spinner slows down instead of stopping, so loading stays visible.
Link buttonText colour--nu-duration-fast · --nu-ease-standardNo transition
Button groupItem background--nu-duration-fast · --nu-ease-standardNo transition
Input, Textarea, Digit inputBackground, border, focus shadow--nu-duration-fast · --nu-ease-standardNo transition
Dropdown menuMenu fades and scales in; switch track and thumb--nu-duration-fast · --nu-ease-standardNo animation or transition
ComboboxChevron rotates--nu-duration-fast · --nu-ease-standardNo transition
Command menuDialog and overlay fade in--nu-duration-fast · --nu-ease-standardNo animation

Gap: the Button spinner doesn't use motion tokens

.nu-button__spinner loops with a hard-coded duration and a plain linear curve, and its reduced-motion duration is also hard-coded. A looping duration token (or --nu-ease-linear plus a documented loop length) is needed before motion can be Stable. Not yet recorded with a decision ID.

Accessibility

Motion must never be the only way a change is shown, and it must stop when people ask for less of it.

Reduced motion

Every @nuton/ui component removes its transitions and animations under prefers-reduced-motion: reduce. The state still changes instantly. The only exception is the Button spinner, which slows down rather than stopping so a loading button still looks busy.

This docs site also shortens every transition and animation to almost nothing under the same setting. Your product needs its own rule.

Animation from interactions

Component motion is small (opacity, a slight scale, colour) and short: the longest token is --nu-duration-slower (500ms). Large movement such as sheets and drawers must also be removed under reduced motion (WCAG 2.3.3).

Flashing

No token or component flashes. Don't build anything that flashes more than three times a second (WCAG 2.3.1).

Moving content

Anything that moves on its own for more than five seconds needs a way to pause it (WCAG 2.2.2). Loading indicators stop when loading ends.

The looping previews on this page are paused by default and play only while you hover or focus their row or card, so nothing moves unless you ask for it. They don't move at all under reduced motion.

You must: add a prefers-reduced-motion: reduce rule for every transition you write; keep the end state visible without the animation; check reduced motion before starting Web Animations. The planned visual regression suite snapshots every component with reduced motion on (quality strategy).

Guidelines and related

Use motion for

  • Feedback on hover, focus, press and selection.
  • Showing where something came from: a menu from its button, a sheet from the edge.
  • Easing an element in or out so the layout change is easy to follow.

Don't use motion for

  • Decoration that doesn't explain a change.
  • The only signal of a state change. Pair it with colour, text or an attribute such as aria-pressed.
  • Delaying people: nothing should wait for an animation to finish before it can be used.

Do and don't

DoUse the fast duration for small changes. Hover and selection feedback should feel immediate.
Don'tDon't use long durations for feedback. A slow hover makes the whole interface feel sluggish.
DoUse the standard curve for things that start and stop. It decelerates into place, so the eye lands on the result.
Don'tDon't use linear for interface movement. Constant speed looks mechanical. Keep it for loops and progress.
DoShow the new state without motion too. With reduced motion on, the pressed fill still says which range is selected.
transition: all 250ms ease-in-out;
Don'tDon't hard-code timing. Raw values drift from the system and skip the reduced-motion review.