Includes the core motion rules plus hover, scroll, and reduced-motion guidance.
Guidelines
Animation Guidelines
Overview
Animation philosophy and duration/easing tokens are defined in foundations/motion.md. This guide covers the operational framework for applying them.
Every animation must pass the three gates before it ships.
The Three Gates
Before adding any animation to a Verdigris surface, it must pass all three checks:
Gate 1: Purpose
“What job does this animation do?”
The answer must be one of:
| Purpose | Example | If you can’t answer… |
|---|---|---|
| Feedback | Button hover state confirms the element is interactive | Remove the animation |
| Orientation | Slide-up reveals content hierarchy on scroll | Remove the animation |
| Brand | Waveform visualization reinforces electrical intelligence | Remove the animation |
If the answer is “it looks nice” or “the competitor does it,” the animation fails Gate 1.
Gate 2: Duration Appropriate?
“Is the timing fast enough to feel responsive but slow enough to be perceived?”
Use the duration decision tree below. If the animation would need a duration outside the token scale (longer than duration.spin at 800ms), reconsider whether it belongs.
Gate 3: Reduced-Motion Fallback
“What happens when
prefers-reduced-motion: reduceis active?”
Every animation must have a defined fallback. See reduced-motion.md for the full guide. If you cannot define a graceful fallback, the animation fails Gate 3.
Duration Decision Tree
Use design tokens from tokens/motion/duration.json:
Is the animation responding to direct user input (hover, click, focus)?
├── YES: Is it a simple state change (opacity, color, focus ring)?
│ ├── YES → duration.fast (150ms)
│ └── NO: Does it involve spatial movement (translate, scale)?
│ ├── Small movement (button hover, input focus) → duration.normal (200ms)
│ └── Larger movement (card lift, image zoom) → duration.moderate (300ms)
└── NO: Is it an entrance/reveal animation?
├── YES: Single element (hero heading, section title) → duration.slow (500ms)
│ Staggered group (card grid, feature list) → duration.slow (500ms) per item, staggered
└── NO: Is it continuous/looping (spinner, progress)?
└── YES → duration.spin (800ms) with easing.linear
Token Reference
| Token | Value | Use |
|---|---|---|
duration.fast |
150ms | Micro-interactions: opacity, focus rings, color shifts |
duration.normal |
200ms | Button hover, input focus, color transitions |
duration.moderate |
300ms | Card hover-lift, image zoom, panel reveal |
duration.slow |
500ms | Hero entrance, page transitions, scroll reveals |
duration.spin |
800ms | Loading spinners (continuous) |
Easing Decision Tree
Use design tokens from tokens/motion/easing.json:
Is the element entering the viewport?
├── YES → easing.out (ease-out) — fast start, gentle landing
└── NO: Is the animation continuous/looping?
├── YES → easing.linear (linear) — constant speed, no acceleration
└── NO → easing.default (ease) — general purpose for state transitions
Token Reference
| Token | Value | Use |
|---|---|---|
easing.default |
ease |
Hover states, color transitions, general interactions |
easing.out |
ease-out |
Entrance animations (slide-up, fade-in, scale-in) |
easing.linear |
linear |
Spinners, progress bars, continuous rotation |
Performance Budgets
CSS Animations
- Animate only
transformandopacity. These properties are compositor-friendly and do not trigger layout or paint. Animatingwidth,height,margin,padding,top,left, orbox-shadowalone causes layout thrashing. - Exception:
box-shadowmay be transitioned alongsidetransformfor hover-lift effects (the shadow change is perceived as part of the elevation shift, and the transform is the primary animation). This is the pattern used in the www codebase:
/* From www index.css — box-shadow transitions alongside transform */
@utility hover-lift {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
- Maximum simultaneous animations: Keep to 3 or fewer elements animating concurrently on mobile. Stagger large groups.
- No animation on page-blocking resources. Hero slide-up uses
animation: slide-up 0.5s ease-out bothwhich is CSS-only and does not block JS parsing.
WebGL / Three.js
For the waveform background (WavesBackground component):
| Budget | Target |
|---|---|
| Frame rate | 60fps on mid-range devices |
| Bundle size | Lazy-loaded (~882KB), never in critical path |
| Fallback | Static gradient for low-power devices or prefers-reduced-motion |
| Mount strategy | Deferred via useState(false) + useEffect to avoid SSR crashes |
Measurement
Use the browser Performance panel to verify:
- No layout shifts caused by animations (CLS impact = 0)
- Animation frames stay under 16ms (60fps target)
- No forced synchronous layouts in animation callbacks
Common Patterns
The Verdigris system uses a small set of repeatable animation patterns. Each is documented in its own file:
| Pattern | File | Token Combination |
|---|---|---|
| Scroll reveal (fade-in, slide-up, scale-in) | scroll-reveal.md | duration.slow + easing.out |
| Hover states (lift, scale, color) | hover-states.md | duration.moderate + easing.default |
| Loading spinners | (inline) | duration.spin + easing.linear |
| Hero entrance | scroll-reveal.md | duration.slow + easing.out |
Spinner Pattern (from Patina)
@keyframes spin-around {
0% { transform: rotate(-90deg); }
100% { transform: rotate(270deg); }
}
.spinner {
animation: spin-around 800ms linear infinite; /* duration.spin + easing.linear */
}
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: none;
/* Show a static indicator instead — e.g., a pulsing opacity or static icon */
}
}
Button Hover (from www)
.button-primary {
transition: all 0.2s ease; /* duration.normal + easing.default */
}
.button-primary:hover {
background-color: hsl(153 67% 32%); /* Darkened primary */
}
Do’s
-
Do: Use the token scale for all durations. Never hardcode a timing value that does not correspond to a token.
- Do: Combine
transformandopacityfor entrance animations. The hero slide-up is the canonical example:@keyframes slide-up { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } -
Do: Gate all hover animations behind the compound media query (see hover-states.md).
- Do: Test every animation with
prefers-reduced-motion: reduceenabled before shipping.
Don’ts
-
Don’t: Animate
width,height,margin, orpadding. These trigger layout recalculation on every frame. -
Don’t: Use animation durations longer than 800ms (
duration.spin). If the animation feels like it needs more time, break it into staggered steps. -
Don’t: Add animation without a
prefers-reduced-motionfallback. This is a hard requirement, not a suggestion. -
Don’t: Use
animation-delayvalues greater than 300ms for interactive feedback. Users perceive delays over 300ms as broken, not animated. -
Don’t: Use JavaScript-driven animation libraries (Framer Motion, GSAP) for effects achievable with CSS transitions and keyframes. The www codebase replaced Framer Motion with CSS
slide-upfor hero animations specifically to eliminate JS from the critical paint path.
Related
- foundations/motion.md — Token rationale and philosophy
- foundations/accessibility.md — WCAG requirements
- scroll-reveal.md — Intersection Observer patterns
- hover-states.md — Hover interaction patterns
- reduced-motion.md — Accessibility fallbacks
- tokens/motion/duration.json — Duration tokens
- tokens/motion/easing.json — Easing tokens