Speed1×
Slow motion multiplier
Reduced motion
Show
Export tuning
proposal defaultsWhat moved
Nothing has been tuned this session
Every value below is the proposal exactly as authored. Change a curve on the tokens page or a spring on the springs page and the differences appear here.
The token block
Highlighted lines are the addition. Core defines tokens throughstylex.defineVars, so this is a diff against two existing objects rather than a :root block — :root would bypass the theme system entirely and could not be retuned by a theme. Semantic durations emit as var(--primitive), which is the shape typeScaleDefaults already uses for type, so a theme that moves the motion scale moves these with it.@astryxdesign/core/src/theme/tokens.stylex.ts — ts// @astryxdesign/core/src/theme/tokens.stylex.ts// Additions only. The nine primitives and --ease-standard do not move.export const easeDefaults = {'--ease-standard': 'cubic-bezier(0.24, 1, 0.4, 1)', // kept; deprecated in docs, not in code'--ease-entry': 'cubic-bezier(0.23, 1, 0.32, 1)','--ease-exit': 'cubic-bezier(0.3, 0, 0.6, 0.6)','--ease-move': 'cubic-bezier(0.77, 0, 0.175, 1)','--ease-state': 'ease','--ease-linear': 'linear','--ease-drawer': 'cubic-bezier(0.32, 0.72, 0, 1)',} as const;export const easeVars = stylex.defineVars(easeDefaults);export const durationDefaults = {'--duration-fast-min': '130ms','--duration-fast': '175ms','--duration-fast-max': '230ms','--duration-medium-min': '310ms','--duration-medium': '410ms','--duration-medium-max': '550ms','--duration-slow-min': '730ms','--duration-slow': '975ms','--duration-slow-max': '1300ms',// Semantic layer. Aliased, so the motion scale a theme sets still owns// these — the same shape typeScaleDefaults uses for type.'--duration-instant': '0ms', // no primitive: the reduced-motion escape'--duration-press': 'var(--duration-fast-min)','--duration-state': 'var(--duration-fast)','--duration-enter': 'var(--duration-fast-max)','--duration-exit': 'var(--duration-fast)','--duration-reveal': 'var(--duration-medium-min)','--duration-overlay': 'var(--duration-medium)','--duration-continuous': 'var(--duration-slow)',} as const;export const durationVars = stylex.defineVars(durationDefaults);// Stagger is a delay between siblings, not a duration, so it gets its own// group rather than widening DurationVarName.export const staggerDefaults = {'--stagger-tight': '30ms','--stagger-base': '50ms','--stagger-loose': '80ms',} as const;export const staggerVars = stylex.defineVars(staggerDefaults);
The JS mirror
Same values, in the shape a JS animation takes. Generated from the theme in the same build step that emits the CSS — see JS token mirror for why it cannot be a handwritten constants file.@astryxdesign/core/motion — generated — ts// @generated from the Astryx theme by scripts/generate-motion-mirror.mjs.// Values are resolved through resolveThemeTokens, so a theme that retunes the// motion scale retunes this file too.export const duration = {instant: 0,press: 0.13,state: 0.175,enter: 0.23,exit: 0.175,reveal: 0.31,overlay: 0.41,continuous: 0.975,} as const;export const ease = {entry: [0.23, 1, 0.32, 1],exit: [0.3, 0, 0.6, 0.6],move: [0.77, 0, 0.175, 1],state: "ease",linear: "linear",drawer: [0.32, 0.72, 0, 1],} as const;export const stagger = {tight: 0.03,base: 0.05,loose: 0.08,} as const;/** No CSS form, so authored here rather than mirrored. */export const spring = {press: {duration: 0.3, bounce: 0.1},swap: {duration: 0.4, bounce: 0.15},panel: {duration: 0.5, bounce: 0.2},layout: {duration: 0.5, bounce: 0.15},} as const;
The lint rule
The token linter already polices type, spacing, radius and colour, and simply has no entry for duration or easing. That is the whole reason 59 values are hardcoded: nothing ever told anyone not to. Six property entries drop straight into the existing pattern map — three rules once the duration, delay and timing-function pairs are grouped. The other three are structural: they are properties of the whole style object, and the current rule only ever looks at one literal at a time.| Rule | Allows | Measured catch | Kind |
|---|---|---|---|
| transitionDuration / animationDuration | var(--duration-*) | 35 hardcoded durations | value |
| transitionDelay / animationDelay | var(--stagger-*), var(--duration-*) | included in the count above | value |
| transitionTimingFunction / animationTimingFunction | var(--ease-*) | 24 hardcoded curves | value |
| motion/require-duration-with-property | a duration beside every transitionProperty | 1 no-op transition | structural |
| motion/require-curve-with-duration | a curve beside every duration | 13 durations with no declared curve | structural |
| motion/no-transition-all | named properties only | 0 today — prevents regressions | structural |
internal/eslint-plugin-xds/index.js — value entries — js// internal/eslint-plugin-xds/index.js — rule: xds/no-hardcoded-styles//// STYLE_PROPERTIES already covers type, spacing, radius and colour. It has no// motion entry, which is why 59 hardcoded duration and easing values// pass lint today. Six additions, in the shape the map already uses:const STYLE_PROPERTIES = {// ...fontSize, padding, borderRadius, color — unchangedtransitionDuration: {pattern: /^['"]?[\d.]+m?s['"]?$/,tokenVar: 'durationVars',message: 'Use a duration token: var(--duration-*)',examples: ["durationVars['--duration-enter']"],},animationDuration: {pattern: /^['"]?[\d.]+m?s['"]?$/,tokenVar: 'durationVars',message: 'Use a duration token: var(--duration-*)',examples: ["durationVars['--duration-continuous']"],},transitionDelay: {pattern: /^['"]?[\d.]+m?s['"]?$/,tokenVar: 'staggerVars',message: 'Use var(--stagger-*) for a group offset, var(--duration-*) otherwise',examples: ["staggerVars['--stagger-base']"],},animationDelay: {pattern: /^['"]?[\d.]+m?s['"]?$/,tokenVar: 'staggerVars',message: 'Use var(--stagger-*) for a group offset, var(--duration-*) otherwise',examples: ["staggerVars['--stagger-tight']"],},transitionTimingFunction: {pattern: /^['"]?(cubic-bezier\([^)]*\)|linear|ease(-in|-out|-in-out)?|steps\([^)]*\))['"]?$/,tokenVar: 'easeVars',message: 'Use an easing token: var(--ease-*)',examples: ["easeVars['--ease-entry']", "easeVars['--ease-exit']"],},animationTimingFunction: {pattern: /^['"]?(cubic-bezier\([^)]*\)|linear|ease(-in|-out|-in-out)?|steps\([^)]*\))['"]?$/,tokenVar: 'easeVars',message: 'Use an easing token: var(--ease-*)',examples: ["easeVars['--ease-linear']"],},};// One carve-out is needed. SKIP_VALUES lets '0', 'none' and 'auto' through for// every property, which is right for padding and wrong for motion: it is what// hides transitionProperty: 'none'. The structural rules below need to see it.
The structural companions — js// A second visitor, because these are properties of the style object rather// than of one value — the existing rule only ever looks at a single literal.'motion/require-duration-with-property': {// transitionProperty with no duration in the same object inherits 0s and// animates nothing. Catches: 1 today.},'motion/require-curve-with-duration': {// A duration with no timing function silently gets the CSS default (ease),// which is a decision nobody made. Catches: 13 today.},'motion/no-transition-all': {// transitionProperty: 'all' animates layout and paint properties nobody// intended. Catches: 0 today — a ratchet, not a cleanup.},