skillby ProfPowell
animation-motion
CSS animations, transitions, and scroll-driven effects with accessibility (prefers-reduced-motion). Use when adding motion, hover effects, loading states, or scroll-based animations.
Installs: 0
Used in: 1 repos
Updated: 2d ago
$
npx ai-builder add skill ProfPowell/animation-motionInstalls to .claude/skills/animation-motion/
# Animation & Motion Skill
This skill covers CSS animations and transitions with a focus on accessibility (respecting user motion preferences) and performance (avoiding jank and layout thrashing).
> **Related:** For CSS-only interactive patterns (tabs, accordions, toggles without JavaScript), see the **`progressive-enhancement`** skill.
## Philosophy
Motion should be:
1. **Purposeful** - Guides attention, shows relationships, provides feedback
2. **Respectful** - Honors `prefers-reduced-motion` preferences
3. **Performant** - Uses compositor-only properties when possible
4. **Subtle** - Enhances, doesn't distract or overwhelm
---
## Reduced Motion First
Always start with reduced motion as the default, then add motion for users who haven't opted out.
### The Pattern
```css
/* Base: no motion (reduced motion default) */
.element {
transition: none;
}
/* Add motion only when user hasn't requested reduced motion */
@media (prefers-reduced-motion: no-preference) {
.element {
transition: transform 0.3s ease, opacity 0.3s ease;
}
}
```
### Why Reduced Motion First?
| Approach | Problem |
|----------|---------|
| Motion first, then remove | Users see flash of motion before media query applies |
| Reduced first, then add | Safe default, motion is progressive enhancement |
---
## Respecting User Preferences
### The `prefers-reduced-motion` Media Query
```css
/* User prefers reduced motion */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
```
### Granular Reduced Motion
Instead of removing all motion, provide alternatives:
```css
/* Full animation for users without preference */
@media (prefers-reduced-motion: no-preference) {
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
}
/* Subtle alternative for reduced motion */
@media (prefers-reduced-motion: reduce) {
.card {
transition: box-shadow 0.15s ease;
}
.card:hover {
box-shadow: var(--shadow-md);
}
}
```
### JavaScript Detection
```javascript
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (prefersReducedMotion) {
// Use instant transitions or skip animations
element.style.transition = 'none';
} else {
// Full animation
element.animate(keyframes, options);
}
```
---
## Performance-Safe Properties
### Compositor-Only Properties (Fast)
These properties can be animated without triggering layout or paint:
| Property | Use For |
|----------|---------|
| `transform` | Movement, scaling, rotation |
| `opacity` | Fade in/out |
| `filter` | Blur, brightness (GPU accelerated) |
```css
/* GOOD: Compositor-only */
.card:hover {
transform: translateY(-4px) scale(1.02);
opacity: 0.9;
}
```
### Properties to Avoid Animating
These trigger expensive layout recalculations:
| Property | Problem |
|----------|---------|
| `width`, `height` | Layout recalc |
| `top`, `left`, `right`, `bottom` | Layout recalc |
| `margin`, `padding` | Layout recalc |
| `border-width` | Layout recalc |
| `font-size` | Layout + text reflow |
```css
/* BAD: Triggers layout */
.card:hover {
margin-top: -4px; /* Layout thrashing */
height: 110%; /* Layout thrashing */
}
/* GOOD: Use transform instead */
.card:hover {
transform: translateY(-4px) scaleY(1.1);
}
```
### Promoting to Compositor Layer
Use `will-change` sparingly for known animations:
```css
/* Only use for elements that WILL animate */
.animated-element {
will-change: transform, opacity;
}
/* Remove after animation completes */
.animated-element.animation-done {
will-change: auto;
}
```
**Warning:** Don't apply `will-change` to many elements—it consumes memory.
---
## Transition Patterns
### Design Token Integration
```css
:root {
/* Duration scale */
--duration-instant: 0.1s;
--duration-fast: 0.15s;
--duration-normal: 0.3s;
--duration-slow: 0.5s;
/* Easing functions */
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
```
### Common Transitions
```css
/* Hover lift effect */
@media (prefers-reduced-motion: no-preference) {
.card {
transition:
transform var(--duration-fast) var(--ease-out),
box-shadow var(--duration-fast) var(--ease-out);
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
}
/* Focus ring */
@media (prefers-reduced-motion: no-preference) {
button {
transition: outline-offset var(--duration-instant) var(--ease-out);
}
button:focus-visible {
outline: 2px solid var(--focus-color);
outline-offset: 2px;
}
}
/* Fade in */
@media (prefers-reduced-motion: no-preference) {
.fade-in {
animation: fadeIn var(--duration-normal) var(--ease-out);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
```
---
## Animation Patterns
### Keyframe Animations
```css
/* Subtle pulse for attention */
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
@media (prefers-reduced-motion: no-preference) {
.notification-badge {
animation: pulse 2s var(--ease-in-out) infinite;
}
}
/* Reduced motion alternative: no animation */
@media (prefers-reduced-motion: reduce) {
.notification-badge {
animation: none;
}
}
```
### Loading Spinners
```css
/* Spinner that respects reduced motion */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-color);
border-top-color: var(--primary-color);
border-radius: 50%;
}
@media (prefers-reduced-motion: no-preference) {
.spinner {
animation: spin 1s linear infinite;
}
}
@media (prefers-reduced-motion: reduce) {
.spinner {
/* Static indicator or pulsing opacity */
animation: none;
border-style: dotted;
}
}
```
### Skeleton Loading
```css
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
var(--surface-color) 25%,
var(--background-alt) 50%,
var(--surface-color) 75%
);
background-size: 200% 100%;
}
@media (prefers-reduced-motion: no-preference) {
.skeleton {
animation: shimmer 1.5s infinite;
}
}
@media (prefers-reduced-motion: reduce) {
.skeleton {
animation: none;
background: var(--background-alt);
}
}
```
---
## Entrance Animations
### Fade and Slide
```css
@keyframes fadeSlideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: no-preference) {
.animate-in {
animation: fadeSlideUp var(--duration-normal) var(--ease-out) both;
}
/* Staggered children */
.animate-in > * {
animation: fadeSlideUp var(--duration-normal) var(--ease-out) both;
}
.animate-in > *:nth-child(1) { animation-delay: 0ms; }
.animate-in > *:nth-child(2) { animation-delay: 50ms; }
.animate-in > *:nth-child(3) { animation-delay: 100ms; }
.animate-in > *:nth-child(4) { animation-delay: 150ms; }
}
@media (prefers-reduced-motion: reduce) {
.animate-in,
.animate-in > * {
animation: none;
opacity: 1;
transform: none;
}
}
```
### Scale In
```css
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@media (prefers-reduced-motion: no-preference) {
dialog[open] {
animation: scaleIn var(--duration-fast) var(--ease-out);
}
}
```
---
## View Transitions API
For page transitions (progressive enhancement):
```css
/* Enable view transitions */
@view-transition {
navigation: auto;
}
/* Default crossfade */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: var(--duration-normal);
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.01ms;
}
}
/* Named transitions for specific elements */
.hero-image {
view-transition-name: hero;
}
::view-transition-old(hero),
::view-transition-new(hero) {
animation-duration: var(--duration-slow);
}
```
---
## Scroll-Driven Animations
Modern CSS scroll-driven animations (progressive enhancement):
```css
/* Fade in on scroll */
@keyframes fadeInOnScroll {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: no-preference) {
.scroll-reveal {
animation: fadeInOnScroll linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.scroll-reveal {
opacity: 1;
transform: none;
}
}
```
---
## Micro-interactions
### Button Press
```css
@media (prefers-reduced-motion: no-preference) {
button {
transition: transform var(--duration-instant) var(--ease-out);
}
button:active {
transform: scale(0.98);
}
}
```
### Toggle Switch
```css
.toggle-track {
width: 44px;
height: 24px;
background: var(--surface-color);
border-radius: 12px;
}
.toggle-thumb {
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transform: translateX(2px);
}
@media (prefers-reduced-motion: no-preference) {
.toggle-thumb {
transition: transform var(--duration-fast) var(--ease-out);
}
}
.toggle-input:checked + .toggle-track .toggle-thumb {
transform: translateX(22px);
}
```
### Checkbox Check
```css
.checkbox-icon {
stroke-dasharray: 24;
stroke-dashoffset: 24;
}
@media (prefers-reduced-motion: no-preference) {
.checkbox-icon {
transition: stroke-dashoffset var(--duration-fast) var(--ease-out);
}
}
.checkbox-input:checked + .checkbox-box .checkbox-icon {
stroke-dashoffset: 0;
}
```
---
## Animation Duration Guidelines
| Animation Type | Duration | Reason |
|---------------|----------|--------|
| Micro-interaction | 100-150ms | Immediate feedback |
| Simple transition | 150-300ms | Noticeable but quick |
| Complex animation | 300-500ms | Time to follow |
| Page transition | 300-500ms | Context shift |
| Loading indicator | 1000-2000ms | One cycle visible |
### The 100ms Rule
Users perceive actions as instant if response is under 100ms. Use this for:
- Button active states
- Focus indicators
- Toggle switches
---
## Dangerous Patterns to Avoid
### Vestibular Triggers
These can cause motion sickness or seizures:
| Pattern | Problem | Alternative |
|---------|---------|-------------|
| Parallax scrolling | Vestibular issues | Static or subtle parallax |
| Auto-playing video | Unexpected motion | Play on interaction |
| Flashing (>3Hz) | Seizure risk | No flashing |
| Large zooming | Vestibular issues | Fade transitions |
| Spinning/rotating | Disorientation | Fade or slide |
```css
/* BAD: Aggressive parallax */
.parallax {
transform: translateY(calc(var(--scroll) * 0.5));
}
/* BETTER: Subtle or disabled with reduced motion */
@media (prefers-reduced-motion: reduce) {
.parallax {
transform: none;
}
}
```
### Infinite Animations
```css
/* BAD: Constant motion */
.attention-seeker {
animation: bounce 1s infinite;
}
/* BETTER: Limited iterations */
.attention-seeker {
animation: bounce 1s 3; /* Only 3 times */
}
/* BEST: Trigger on interaction */
.attention-seeker:hover {
animation: bounce 0.5s;
}
```
---
## Testing Checklist
### Browser DevTools
1. **Chrome**: Rendering > Emulate CSS media feature > prefers-reduced-motion: reduce
2. **Firefox**: about:config > ui.prefersReducedMotion (0=no-preference, 1=reduce)
3. **Safari**: Develop > Experimental Features > Reduced Motion
### System Settings
- **macOS**: System Preferences > Accessibility > Display > Reduce motion
- **iOS**: Settings > Accessibility > Motion > Reduce Motion
- **Windows**: Settings > Ease of Access > Display > Show animations
- **Android**: Settings > Accessibility > Remove animations
---
## Checklist
When adding animations or transitions:
- [ ] `prefers-reduced-motion` is respected
- [ ] Reduced motion has a meaningful alternative (not just disabled)
- [ ] Only compositor properties are animated (`transform`, `opacity`)
- [ ] `will-change` is used sparingly and removed after animation
- [ ] Duration tokens are used consistently
- [ ] No flashing content (>3 flashes per second)
- [ ] Infinite animations have a purpose and can be stopped
- [ ] Parallax and large motion are optional enhancements
- [ ] Loading states work without animation
- [ ] Animation enhances rather than distracts
## Related Skills
- **css-author** - Modern CSS organization with native @import, @layer casca...
- **progressive-enhancement** - HTML-first development with CSS-only interactivity patterns
- **performance** - Write performance-friendly HTML pages
- **accessibility-checker** - Ensure WCAG2AA accessibility complianceQuick Install
$
npx ai-builder add skill ProfPowell/animation-motionDetails
- Type
- skill
- Author
- ProfPowell
- Slug
- ProfPowell/animation-motion
- Created
- 6d ago