Agent skill
motion-scale
Generates animation duration, easing curves, and delay tokens with prefers-reduced-motion support. Use when creating transition timing, animation speed, or motion systems. Outputs CSS, Tailwind, or JSON.
Install this agent skill to your Project
npx add-skill https://github.com/dylantarre/design-system-skills/tree/main/skills/tokens/motion-scale
SKILL.md
Motion Scale Generator
Overview
Generate consistent animation and transition tokens for duration, easing, and delay. Creates a motion system that feels cohesive and respects user preferences for reduced motion.
When to Use
- Setting up animation tokens for a new project
- Standardizing transition timing across components
- Replacing hardcoded animation values
- Implementing reduced motion support
- Building micro-interaction patterns
Quick Reference: Duration Scale
| Token | Value | Use Case |
|---|---|---|
instant |
0ms | Immediate, no animation |
fastest |
50ms | Micro-feedback (ripples, highlights) |
fast |
100ms | Hover, focus, small changes |
normal |
200ms | Most transitions, toggles |
slow |
300ms | Modals, drawers, reveals |
slower |
400ms | Page transitions, complex sequences |
slowest |
500ms | Elaborate animations, onboarding |
Quick Reference: Easing Curves
| Token | Curve | Use Case |
|---|---|---|
linear |
linear |
Progress bars, looping |
ease-in |
cubic-bezier(0.4, 0, 1, 1) |
Exit animations |
ease-out |
cubic-bezier(0, 0, 0.2, 1) |
Enter animations (most common) |
ease-in-out |
cubic-bezier(0.4, 0, 0.2, 1) |
Move, resize, continuous |
ease-bounce |
cubic-bezier(0.34, 1.56, 0.64, 1) |
Playful, attention |
ease-elastic |
cubic-bezier(0.68, -0.55, 0.27, 1.55) |
Springy, overshoot |
The Process
- Choose duration scale: How many steps? (5-7 typical)
- Define base duration: 200ms is standard baseline
- Select ratio: How durations scale (1.5x or 2x)
- Pick easing curves: Standard set + any special curves
- Create semantic aliases: Map to component behaviors
- Add reduced motion: Respect user preferences
- Choose format: CSS, Tailwind, or JSON
Output Formats
CSS Custom Properties:
:root {
/* ===== Durations ===== */
--duration-0: 0ms;
--duration-50: 50ms;
--duration-100: 100ms;
--duration-150: 150ms;
--duration-200: 200ms;
--duration-300: 300ms;
--duration-400: 400ms;
--duration-500: 500ms;
--duration-700: 700ms;
--duration-1000: 1000ms;
/* Named aliases */
--duration-instant: var(--duration-0);
--duration-fastest: var(--duration-50);
--duration-fast: var(--duration-100);
--duration-normal: var(--duration-200);
--duration-slow: var(--duration-300);
--duration-slower: var(--duration-500);
--duration-slowest: var(--duration-700);
/* ===== Easings ===== */
--ease-linear: linear;
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
/* Expressive easings */
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-elastic: cubic-bezier(0.68, -0.55, 0.27, 1.55);
--ease-snap: cubic-bezier(0.2, 0, 0, 1);
/* Deceleration/Acceleration (Material Design style) */
--ease-decelerate: cubic-bezier(0, 0, 0.2, 1);
--ease-accelerate: cubic-bezier(0.4, 0, 1, 1);
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
/* ===== Delays ===== */
--delay-none: 0ms;
--delay-short: 50ms;
--delay-medium: 100ms;
--delay-long: 200ms;
/* Stagger delays for sequential animations */
--delay-stagger-1: 0ms;
--delay-stagger-2: 50ms;
--delay-stagger-3: 100ms;
--delay-stagger-4: 150ms;
--delay-stagger-5: 200ms;
/* ===== Semantic Transitions ===== */
--transition-colors: color, background-color, border-color, fill, stroke;
--transition-opacity: opacity;
--transition-transform: transform;
--transition-shadow: box-shadow;
--transition-all: all;
/* Component transitions */
--transition-hover: var(--duration-fast) var(--ease-out);
--transition-focus: var(--duration-fast) var(--ease-out);
--transition-active: var(--duration-fastest) var(--ease-out);
--transition-enter: var(--duration-normal) var(--ease-out);
--transition-exit: var(--duration-fast) var(--ease-in);
--transition-move: var(--duration-normal) var(--ease-in-out);
--transition-expand: var(--duration-slow) var(--ease-out);
--transition-collapse: var(--duration-normal) var(--ease-in);
}
/* ===== Reduced Motion ===== */
@media (prefers-reduced-motion: reduce) {
:root {
--duration-50: 0ms;
--duration-100: 0ms;
--duration-150: 0ms;
--duration-200: 0ms;
--duration-300: 0ms;
--duration-400: 0ms;
--duration-500: 0ms;
--duration-700: 0ms;
--duration-1000: 0ms;
}
}
/* Alternative: Keep minimal motion */
@media (prefers-reduced-motion: reduce) {
:root {
--duration-fastest: 0ms;
--duration-fast: 0ms;
--duration-normal: 50ms; /* Minimal feedback */
--duration-slow: 50ms;
--duration-slower: 100ms;
--duration-slowest: 100ms;
/* Disable expressive easings */
--ease-bounce: var(--ease-out);
--ease-elastic: var(--ease-out);
}
}
Tailwind Config:
module.exports = {
theme: {
extend: {
transitionDuration: {
'0': '0ms',
'50': '50ms',
'100': '100ms',
'150': '150ms',
'200': '200ms',
'300': '300ms',
'400': '400ms',
'500': '500ms',
'700': '700ms',
'1000': '1000ms',
},
transitionTimingFunction: {
'in': 'cubic-bezier(0.4, 0, 1, 1)',
'out': 'cubic-bezier(0, 0, 0.2, 1)',
'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
'bounce': 'cubic-bezier(0.34, 1.56, 0.64, 1)',
'elastic': 'cubic-bezier(0.68, -0.55, 0.27, 1.55)',
'snap': 'cubic-bezier(0.2, 0, 0, 1)',
},
transitionDelay: {
'0': '0ms',
'50': '50ms',
'100': '100ms',
'150': '150ms',
'200': '200ms',
},
animation: {
'fade-in': 'fade-in 200ms ease-out',
'fade-out': 'fade-out 150ms ease-in',
'slide-up': 'slide-up 200ms ease-out',
'slide-down': 'slide-down 200ms ease-out',
'scale-in': 'scale-in 200ms ease-out',
'spin-slow': 'spin 2s linear infinite',
},
keyframes: {
'fade-in': {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
'fade-out': {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
'slide-up': {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
'slide-down': {
'0%': { transform: 'translateY(-10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
'scale-in': {
'0%': { transform: 'scale(0.95)', opacity: '0' },
'100%': { transform: 'scale(1)', opacity: '1' },
},
},
},
},
};
JSON Tokens:
{
"motion": {
"duration": {
"0": { "value": "0ms", "type": "duration" },
"50": { "value": "50ms", "type": "duration" },
"100": { "value": "100ms", "type": "duration" },
"200": { "value": "200ms", "type": "duration" },
"300": { "value": "300ms", "type": "duration" },
"500": { "value": "500ms", "type": "duration" }
},
"easing": {
"linear": { "value": "linear", "type": "cubicBezier" },
"ease-in": { "value": "cubic-bezier(0.4, 0, 1, 1)", "type": "cubicBezier" },
"ease-out": { "value": "cubic-bezier(0, 0, 0.2, 1)", "type": "cubicBezier" },
"ease-in-out": { "value": "cubic-bezier(0.4, 0, 0.2, 1)", "type": "cubicBezier" },
"bounce": { "value": "cubic-bezier(0.34, 1.56, 0.64, 1)", "type": "cubicBezier" }
},
"delay": {
"none": { "value": "0ms", "type": "duration" },
"short": { "value": "50ms", "type": "duration" },
"medium": { "value": "100ms", "type": "duration" }
}
}
}
Easing Curve Reference
Standard Curves
ease-out (enter/appear):
●━━━━━━━━━━━━━━━━━━●
Fast start, gentle stop
Best for: Elements entering view
ease-in (exit/leave):
●━━━━━━━━━━━━━━━━━━●
Gentle start, fast end
Best for: Elements leaving view
ease-in-out (move/resize):
●━━━━━━━━━━━━━━━━━━●
Gentle start and end
Best for: Elements moving position
linear (progress):
●━━━━━━━━━━━━━━━━━━●
Constant speed
Best for: Progress bars, spinners
Expressive Curves
bounce (playful):
●━━━━━━━━╮
╰━━━●
Overshoots then settles
Best for: Notifications, confirmations
elastic (springy):
●━━━╮ ╭━╮
╰━━━╯ ╰●
Oscillates before settling
Best for: Attention, drag-and-drop
snap (decisive):
●━━━━━━━━━━━━━━━━●
Very fast deceleration
Best for: Snapping, selections
Custom Curve Creation
/*
cubic-bezier(x1, y1, x2, y2)
x1, y1 = First control point (affects start)
x2, y2 = Second control point (affects end)
y values > 1 = overshoot
y values < 0 = anticipation
*/
/* Dramatic entrance */
--ease-dramatic: cubic-bezier(0.2, 0.8, 0.2, 1);
/* Snappy with slight overshoot */
--ease-snappy: cubic-bezier(0.2, 1.2, 0.3, 1);
/* Gentle float */
--ease-float: cubic-bezier(0.4, 0.2, 0.2, 1);
Component Patterns
Button Hover
.button {
transition:
background-color var(--transition-hover),
transform var(--transition-active);
}
.button:hover {
background-color: var(--color-primary-600);
}
.button:active {
transform: scale(0.98);
}
Modal Enter/Exit
.modal-backdrop {
transition: opacity var(--duration-normal) var(--ease-out);
}
.modal-content {
transition:
opacity var(--duration-normal) var(--ease-out),
transform var(--duration-normal) var(--ease-out);
}
/* Enter */
.modal-backdrop.entering { opacity: 0; }
.modal-backdrop.entered { opacity: 1; }
.modal-content.entering { opacity: 0; transform: scale(0.95) translateY(10px); }
.modal-content.entered { opacity: 1; transform: scale(1) translateY(0); }
/* Exit (faster) */
.modal-backdrop.exiting {
opacity: 0;
transition-duration: var(--duration-fast);
}
.modal-content.exiting {
opacity: 0;
transform: scale(0.95);
transition-duration: var(--duration-fast);
}
Dropdown Menu
.dropdown-menu {
transform-origin: top;
transition:
opacity var(--duration-fast) var(--ease-out),
transform var(--duration-fast) var(--ease-out);
}
.dropdown-menu[data-state="closed"] {
opacity: 0;
transform: scaleY(0.9);
pointer-events: none;
}
.dropdown-menu[data-state="open"] {
opacity: 1;
transform: scaleY(1);
}
Staggered List Animation
.list-item {
opacity: 0;
transform: translateY(10px);
animation: slide-up var(--duration-normal) var(--ease-out) forwards;
}
.list-item:nth-child(1) { animation-delay: var(--delay-stagger-1); }
.list-item:nth-child(2) { animation-delay: var(--delay-stagger-2); }
.list-item:nth-child(3) { animation-delay: var(--delay-stagger-3); }
.list-item:nth-child(4) { animation-delay: var(--delay-stagger-4); }
.list-item:nth-child(5) { animation-delay: var(--delay-stagger-5); }
@keyframes slide-up {
to {
opacity: 1;
transform: translateY(0);
}
}
Loading Spinner
.spinner {
animation: spin var(--duration-1000) var(--ease-linear) infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Pulsing loader */
.pulse {
animation: pulse var(--duration-700) var(--ease-in-out) infinite alternate;
}
@keyframes pulse {
0% { opacity: 0.4; transform: scale(0.95); }
100% { opacity: 1; transform: scale(1); }
}
Skeleton Loading
.skeleton {
background: linear-gradient(
90deg,
var(--color-gray-200) 0%,
var(--color-gray-100) 50%,
var(--color-gray-200) 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s var(--ease-in-out) infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Reduced Motion
Strategy 1: Remove All Motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Strategy 2: Reduce, Don't Remove
@media (prefers-reduced-motion: reduce) {
:root {
/* Keep brief transitions for feedback */
--duration-fast: 0ms;
--duration-normal: 50ms;
--duration-slow: 100ms;
/* Simplify easings */
--ease-bounce: var(--ease-out);
--ease-elastic: var(--ease-out);
}
/* Disable parallax, auto-play, infinite loops */
.parallax { transform: none !important; }
.auto-animate { animation: none !important; }
}
Strategy 3: Provide Alternatives
/* Full motion */
.card-enter {
animation: slide-up var(--duration-normal) var(--ease-out);
}
/* Reduced: fade only, no movement */
@media (prefers-reduced-motion: reduce) {
.card-enter {
animation: fade-in var(--duration-fast) var(--ease-out);
}
}
Duration Guidelines
| Duration | Feel | Use For |
|---|---|---|
| 0-50ms | Instant | Ripples, micro-feedback |
| 50-100ms | Snappy | Hover states, focus, toggles |
| 100-200ms | Quick | Most UI transitions |
| 200-300ms | Smooth | Modals, panels, reveals |
| 300-500ms | Deliberate | Page transitions, onboarding |
| 500ms+ | Slow | Complex sequences, emphasis |
Rule of thumb:
- Smaller elements = faster (50-150ms)
- Larger elements = slower (200-400ms)
- User-triggered = fast (100-200ms)
- System-triggered = can be slower (200-400ms)
Testing Checklist
- Transitions feel smooth, not jarring
- Hover/focus feedback is immediate (<150ms)
- Modals/drawers don't feel sluggish
- Animations enhance, don't block interaction
-
prefers-reduced-motionis respected - No layout shift during animations
- Animations work at 60fps
- Consistent timing across similar interactions
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
getting-started
Introduction to the design system skills plugin. Use when first installing the plugin, exploring available skills, or planning a design system implementation.
responsive-typography
Implements fluid typography using CSS clamp() that scales with viewport. Use when creating responsive font sizes, viewport-aware headings, or type that adapts smoothly without breakpoints.
spacing-scale
Generates consistent spacing tokens using base values and ratios. Use when creating margin/padding systems, gap tokens, or layout spacing. Outputs CSS custom properties, Tailwind config, or JSON tokens.
color-scale
Generates perceptually uniform OKLCH color palettes from brand colors. Use when creating color systems, theme palettes, or converting hex values to 11-step scales. Outputs CSS custom properties, Tailwind config, or JSON tokens.
shadow-scale
Generates layered box-shadow elevation tokens for depth hierarchy. Use when creating elevation systems, card shadows, modal depth, or dark mode shadow variants. Outputs CSS, Tailwind, or JSON.
breakpoints
Generates responsive breakpoint tokens for media queries and container queries. Use when setting up responsive design, mobile-first layouts, or viewport-based styling. Outputs CSS, Tailwind, or JSON.
Didn't find tool you were looking for?