Motion
Free · MITTrue when a family has one way of drawing its change (fill, move, press, tint, refuse, attention, live and clock). Its single variant is fixed: there is no -variant token to set, and motion() writes none, because the family's utility has nothing to choose between. The docs mark such a family "(fixed)" instead of listing a value that does nothing. A later release that adds a second word turns the token on without a new name.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
- 5.1.0
- 5.0.0
- 4.5.0
- 4.4.0
- 4.3.0
- 4.2.0
- 4.1.0
- 4.0.2
- 4.0.1
- 4.0.0
- 3.0.0
- 2.10.0
- 2.9.0
- 2.8.0
- 2.7.0
- 2.6.2
- 2.6.1
- 2.6.0
- 2.5.1
- 2.5.0
- 2.4.0
- 2.3.0
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0 and 2.2.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
It's types and helpers, listed below.
Install
Opens the free starter app in StackBlitz, in your browser, on stackblitz.com. Motion isn’t added for you: run npx candor-ui add motion in its terminal. The starter’s agent needs an Anthropic API key to chat; without one it shows the components only.
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/motion
Functions
motionVariantIsFixed(family: MotionFamily)motionHasVariant(family: MotionFamily)True when a family's chain carries a -variant: it chooses between drawings, or (4.4, drawable) its one free word can be swapped for a drawing from the Pro motion-drawings item. Attention is both fixed and drawable: the free kit has only its ring, and --cd-motion-attention-variant falls back to cd-ring.
motionVariantValue(variant: string)The CSS value a variant is written as. CSS can only choose a drawing by keyframe name, so a variant's value is its keyframe's: the word with Candor's prefix, which keeps an app's own @keyframes rise from taking over.
motionMomentOf(style: MotionStyle | undefined)The moment a chain was built for, or undefined for a plain style object.
motion(item: string, moment: string, family: F, defaults: MomentDefaults<F> = {})Builds a moment's chain. Call it once at module level and spread the result into the element's style, beside the family utility that the component adds at the honest moment:
const tick = motion("copy-field", "tick", "pop")
<Check style={tick} className={cn(copied && "cd-pop")} />The brand reaches the moment with --cd-motion-copy-field-tick (its level), -dur, -ease, the family's amplitude suffix and -variant; anything it doesn't set falls back to the family (--cd-motion-pop-*), then to defaults, then to Candor's. Two families on one element compose: style={{ ...press, ...tint }}.
useSettled(value: T, test?: SettledTest<T>)useArrived(keys: readonly string[])Which keys were added after the component first drew. Keys present at the first render answer false, so a list that loads with fifty entries draws them still; a key added later answers true until it leaves (and true again if it comes back).
useLeaving(items: readonly T[], keyOf: (item: T) => string, fallbackMs = 600)Items that just left, kept as inert ghosts for the length of their out moment. Each ghost goes when left(item) is called (from its onAnimationEnd), or after fallbackMs when nothing animated. The default, 600 ms, is longer than the longest out moment a brand can set (pop's 300 ms at the slowest speed, 1.5×, is 450 ms). An item that comes back while its ghost is still leaving takes the ghost's place.
motionMode(el: Element | null | undefined)The person's setting where el is: none under data-motion="none", calm under the OS's reduced-motion setting or data-motion="calm", else full. none or calm anywhere above wins (the same reach as the CSS variants), the OS setting can't be raised by the app, and values that aren't Candor's are ignored (an app's own Radix menus, beside Candor's Base UI ones, write data-motion on their panels for the direction they opened from).
useMotionMode(ref: React.RefObject<Element | null>)motionMode for a component that has to react to it: the person's setting where ref's element is, read after mount and again whenever the app changes data-motion or the OS setting changes. It starts as full for the first render, since there is nothing to read until the element exists. Use it for behaviour (a timer that must stop), not for styling, which the CSS variants already answer.
motionLevel(el: Element | null | undefined, moment: MotionMoment | MotionFamily)The level a moment resolves to on el, read from its computed style: the moment's token, its family's, the preset, then capped by data-motion. A page without Candor's CSS reads as subtle.
useMotionAllowed(ref: React.RefObject<Element | null>, moment: MotionStyle | MotionFamily)For motion that CSS can't stop (smooth scrolling, the Web Animations API, a bar painted from the clock): a stable function that answers, at the moment of the motion, whether it may play on ref's element. False under data-motion="none", under the calm level (reduced motion), and when the moment's resolved level is 0. Pass the moment's chain from motion(), or a family name for the family's level.
Values
motionEasesThe CSS each easing name stands for, as a brand file writes it.
Readonly<Record<MotionEase, string>>motionFamiliesThe family defaults. Durations, easings and amplitudes are 2.2's, so an app with no brand file looks the same.
{
readonly reveal: {
readonly reports: "Content opened or closed to its measured size; a chevron turns.";
readonly spatial: true;
readonly essential: false;
readonly dur: {
readonly default: 200;
readonly min: 150;
readonly max: 300;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard", "inout"];
readonly amp: null;
readonly variants: readonly ["size", "fade"];
readonly calm: "Snaps to its measured size.";
};
readonly arrive: {
readonly reports: "Something new, added after the first draw, or arriving as the page draws when it is motion for show.";
readonly spatial: true;
readonly essential: false;
readonly dur: {
readonly default: 240;
readonly min: 150;
readonly max: 300;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard", "spring"];
readonly amp: {
readonly name: "distance";
readonly default: "8px";
readonly min: 0;
readonly max: 16;
readonly unit: "px";
readonly subtle: 0.5;
};
readonly variants: readonly ["rise", "fade", "pop"];
readonly calm: "Fades in, up to 150 ms.";
};
readonly pop: {
readonly reports: "Added, removed, or a small mark appearing.";
readonly spatial: true;
readonly essential: false;
readonly dur: {
readonly default: 200;
readonly min: 100;
readonly max: 300;
};
readonly outDur: {
readonly default: 120;
readonly min: 80;
readonly max: 300;
};
readonly ease: "standard";
readonly eases: readonly ["standard", "spring"];
readonly amp: {
readonly name: "scale";
readonly default: "0.85";
readonly min: 0.8;
readonly max: 1;
readonly unit: "";
readonly subtle: 0;
};
readonly variants: readonly ["scale", "fade"];
readonly calm: "Fades, up to 150 ms.";
};
readonly mark: {
readonly reports: "A tick or cross drawn when a result settles.";
readonly spatial: true;
readonly essential: false;
readonly dur: {
readonly default: 360;
readonly min: 200;
readonly max: 500;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard"];
readonly amp: null;
readonly variants: readonly ["draw", "pop", "drop"];
readonly calm: "The finished mark fades in, up to 150 ms.";
};
readonly fill: {
readonly reports: "A bar or ring moving between measured values.";
readonly spatial: true;
readonly essential: false;
readonly dur: {
readonly default: 360;
readonly min: 200;
readonly max: 600;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard", "inout"];
readonly amp: null;
readonly variants: readonly ["grow"];
readonly calm: "Snaps to the new value; its colour may fade.";
};
readonly value: {
readonly reports: "A number or word changed in place.";
readonly spatial: false;
readonly essential: false;
readonly dur: {
readonly default: 120;
readonly min: 80;
readonly max: 200;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard"];
readonly amp: {
readonly name: "distance";
readonly default: "2px";
readonly min: 0;
readonly max: 4;
readonly unit: "px";
readonly subtle: 0;
};
readonly variants: readonly ["fade", "slide"];
readonly calm: "Crossfades, up to 150 ms.";
};
readonly move: {
readonly reports: "A thing changed place: a reorder, a tab indicator, a toast restacking, a scroll.";
readonly spatial: true;
readonly essential: false;
readonly dur: {
readonly default: 200;
readonly min: 150;
readonly max: 300;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard", "spring"];
readonly amp: null;
readonly variants: readonly ["slide"];
readonly calm: "Jumps to its new place.";
};
readonly overlay: {
readonly reports: "A dialog, sheet, popover, menu or tooltip opened or closed.";
readonly spatial: true;
readonly essential: false;
readonly dur: {
readonly default: 150;
readonly min: 100;
readonly max: 500;
};
readonly outDur: {
readonly default: 150;
readonly min: 80;
readonly max: 300;
};
readonly ease: "standard";
readonly eases: readonly ["standard", "inout", "spring"];
readonly amp: {
readonly name: "scale";
readonly default: "0.95";
readonly min: 0.9;
readonly max: 1;
readonly unit: "";
readonly subtle: 1;
};
readonly variants: readonly ["scale", "slide", "fade"];
readonly calm: "Fades, up to 150 ms.";
};
readonly press: {
readonly reports: "A press registered.";
readonly spatial: true;
readonly essential: false;
readonly dur: {
readonly default: 150;
readonly min: 80;
readonly max: 200;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard"];
readonly amp: {
readonly name: "scale";
readonly default: "0.96";
readonly min: 0.9;
readonly max: 1;
readonly unit: "";
readonly subtle: 0.5;
};
readonly variants: readonly ["scale"];
readonly calm: "Doesn't scale; the colour still answers.";
};
readonly tint: {
readonly reports: "Hover, focus, selected, a drop target: a colour or opacity change.";
readonly spatial: false;
readonly essential: false;
readonly dur: {
readonly default: 150;
readonly min: 0;
readonly max: 200;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard", "inout"];
readonly amp: null;
readonly variants: readonly ["fade"];
readonly calm: "Colour and opacity only, up to 150 ms.";
};
readonly refuse: {
readonly reports: "A locked control was pressed: the reason is shown, and under expressive it shakes.";
readonly spatial: true;
readonly essential: false;
readonly dur: {
readonly default: 360;
readonly min: 200;
readonly max: 500;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard"];
readonly amp: {
readonly name: "distance";
readonly default: "3px";
readonly min: 0;
readonly max: 6;
readonly unit: "px";
readonly subtle: 0;
};
readonly variants: readonly ["shake"];
readonly calm: "No shake; the reason's colour still changes.";
};
readonly attention: {
readonly reports: "Something new waits on the person: a ring that grows and fades.";
readonly spatial: false;
readonly essential: false;
readonly dur: {
readonly default: 900;
readonly min: 600;
readonly max: 1600;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard"];
readonly amp: {
readonly name: "count";
readonly default: "1";
readonly min: 1;
readonly max: 3;
readonly unit: "";
readonly subtle: 1;
};
readonly variants: readonly ["ring"];
readonly calm: "One ring that fades without growing, up to 150 ms.";
readonly drawable: true;
};
readonly live: {
readonly reports: "Work that is being heard from: a dot's pulse, a caret, a spinner, a shimmer, a skeleton.";
readonly spatial: false;
readonly essential: false;
readonly dur: {
readonly default: 1600;
readonly min: 1000;
readonly max: 3000;
};
readonly outDur: null;
readonly ease: "standard";
readonly eases: readonly ["standard", "inout", "linear"];
readonly amp: null;
readonly variants: readonly ["pulse"];
readonly calm: "Still, with its word.";
};
readonly clock: {
readonly reports: "Real time passing: a hold, a deadline, a replay. Painted from the clock.";
readonly spatial: true;
readonly essential: true;
readonly dur: null;
readonly outDur: null;
readonly ease: "linear";
readonly eases: readonly ["linear"];
readonly amp: null;
readonly variants: readonly ["glide"];
readonly calm: "Steps instead of gliding; never off.";
};
}motionFamilyNamesThe family names, in the order the docs and the theme builder list them.
MotionFamily[]Types
- MotionFamily
- The fourteen families: one per kind of change a person can see.
| "reveal" | "arrive" | "pop" | "mark" | "fill" | "value" | "move" | "overlay" | "press" | "tint" | "refuse" | "attention" | "live" | "clock"- MotionLevel
- A motion level: 0 off (the end state appears, events still fire), 1 subtle, 2 expressive.
0 | 1 | 2- MotionEase
- The easings a family may allow. Spring overshoots, so only spatial families accept it.
"standard" | "inout" | "spring" | "linear"- MotionAmplitude
- A family's amplitude: what its expressive level moves by.
{ /** The token suffix: `--cd-motion-<family>-<name>`. */ readonly name: "distance" | "scale" | "count"; /** The value at the expressive level, as CSS. */ readonly default: string; /** The lowest value `theme check` accepts. */ readonly min: number; /** The highest value `theme check` accepts. */ readonly max: number; /** The unit `min` and `max` are in ("" for a plain number). */ readonly unit: "px" | "" | "%"; /** How much of the amplitude the subtle level uses: 0 none, 1 all of it. */ readonly subtle: number; }- MotionDuration
- A duration with the range `theme check` accepts, in milliseconds.
{ readonly default: number; readonly min: number; readonly max: number }- MotionFamilySpec
- Everything Candor knows about one family. `check:motion` checks `candor.css` against these.
{ /** What the family reports, in words, for the docs and the theme builder. */ readonly reports: string; /** Moves things in space (scale, translate, size). Spring easing is accepted only here. */ readonly spatial: boolean; /** The information itself (a hold, a deadline): a brand can't turn it below subtle. */ readonly essential: boolean; /** How long one play takes; `null` when the clock decides. */ readonly dur: MotionDuration | null; /** How long leaving takes, for families with an out moment (`-out-dur`). */ readonly outDur: MotionDuration | null; /** The easing used when nothing sets one. */ readonly ease: MotionEase; /** The easings a brand may choose. */ readonly eases: readonly MotionEase[]; /** The amplitude token, when the family has one. */ readonly amp: MotionAmplitude | null; /** The closed set of variants: how the change is drawn, never when. The first is the default. */ readonly variants: readonly [ string, ...string[] ]; /** What the calm level (reduced motion) does with it, in words. */ readonly calm: string; /** * Its utility plays a keyframe a brand can swap for a Pro drawing, though the family has one free word (attention's * ring, from 4.4). Its chain carries a `-variant` that falls back to that word, so nothing changes without a brand * value; it stays fixed in the free kit. */ readonly drawable?: boolean | undefined; }- MotionVariant
- A family's variants, by name: `MotionVariant<"mark">` is `"draw" | "pop" | "drop"`.
(typeof motionFamilies)[F]["variants"][number]- MomentDefaults
- What a moment may set as its own default, where it differs from its family's.
{ /** The moment's duration, in milliseconds, within its family's range. */ dur?: number | undefined; /** How long leaving takes, for families with an out moment. */ outDur?: number | undefined; /** The moment's easing, from its family's allowed set. */ ease?: (typeof motionFamilies)[F]["eases"][number] | undefined; /** The amplitude at the expressive level, as CSS (`"0.9"`, `"0.25rem"`, `"3"`). */ amp?: string | undefined; /** The moment's variant, from its family's set. */ variant?: MotionVariant<F> | undefined; }- MotionMoment
- The moment a chain belongs to, kept on the style object (not enumerable, so it never reaches the DOM).
{ readonly item: string; readonly moment: string; readonly family: MotionFamily }- MotionStyle
- A moment's chain: CSS variables to spread into the moving element's `style`. Each resolves the moment's own token, then its family's, then the default, on that element, so a `data-motion` container anywhere above it still applies.
React.CSSProperties & { readonly [momentKey]?: MotionMoment | undefined }- SettledTest
- How `useSettled` tells a finished value from an unfinished one; Candor's run statuses when left out.
{ /** Reached an outcome worth showing settling (done or failed). */ settled: (value: T) => boolean; /** Over in any way, including ones that don't animate (cancelled, skipped); `settled` when left out. */ over?: ((value: T) => boolean) | undefined; }- MotionMode
- What the person allows here: everything, the calm level (reduced motion), or nothing.
"full" | "calm" | "none"
Terms used here
- Calm level
- What Candor's motion does for a person whose operating system asks for reduced motion: the highest-precedence setting, above the app's own motion level (
none,calm,subtleorexpressive) and the brand file. A theme tunes how a change looks, never when it happens, so nothing depends on seeing the motion.
Every term is defined in the glossary (Markdown).