CSS Animations skill for Codex

Smooth motion with zero JavaScript. Codex picks between transitions, keyframes, @starting-style and scroll timelines based on what the motion needs, animates only compositor-friendly properties, and wraps newer features in @supports so Firefox or older Safari get a correct static page. Reduced-motion handling is built in from the start and checked in DevTools rather than bolted on.

  • Frontend
  • $css-animations
  • 209 lines
  • 2 files
  • ~2,250 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- css-animations

Installs to ~/.agents/skills/css-animations/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $css-animations, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds and fixes CSS transitions, @keyframes, scroll-driven animations (animation-timeline scroll() and view()), @starting-style entry effects and @property-animated custom properties, keeping motion on the compositor and honoring prefers-reduced-motion. Use when a user asks to animate an element without JavaScript, add a fade or slide on enter, animate to height auto, build a scroll progress bar or reveal-on-scroll, fix janky or stuttering CSS animation, or make motion accessible.

Use it when

  • An interaction needs hover, focus or open/close motion in plain CSS
  • Content should reveal or progress with scroll without a JS scroll listener
  • An existing CSS animation stutters, flashes on load or ignores reduced motion

Use something else when

How to check the result

Record the interaction in the DevTools Performance panel with paint flashing on, confirm no per-frame layout work, then emulate prefers-reduced-motion and check the static fallback in a non-supporting browser.

The mistake it prevents

Writing transition: all, or placing animation-timeline before the animation shorthand, which silently resets it to auto.

~/.agents/skills/css-animations/SKILL.md

--- name: css-animations description: Builds and fixes CSS transitions, @keyframes, scroll-driven animations (animation-timeline scroll() and view()), @starting-style entry effects and… license: MIT ---

Done well means: every animation runs on transform, opacity or filter (or is short and off the critical path), there is a reduced-motion path, nothing flashes or jumps on first paint, and the effect degrades to a static but correct UI where a feature is unsupported.

Before writing anything

  1. Read AGENTS.md and the styling setup (plain CSS, CSS Modules, Tailwind, CSS-in-JS). Put keyframes where the project already keeps them; with Tailwind v4 that is usually @theme (--animate-*) plus @keyframes in the main CSS file.
  2. Search for existing motion tokens (--duration-*, --ease-*) and reuse them.
  3. Check the browserslist / support policy. It decides whether scroll-driven animations and @starting-style can be the only path or must be progressive enhancement.
  4. Decide if CSS is the right tool (table below) before writing code.

CSS or JavaScript?

NeedUse
Hover, focus, pressed, open/closed state changeCSS transition
Looping or multi-step decorative motion@keyframes
Element appears (inserted, display:none → block, popover, dialog)@starting-style + transition-behavior: allow-discrete
Progress tied to scroll position or element visibilityanimation-timeline: scroll() / view()
Exit animation for an element React/Vue removes from the DOMJS library ($motion-animations) or delay removal until animationend
Sequenced timelines, scrubbing, pinning, physics, drag$gsap-animations or $motion-animations
Page or route change between two DOM states$view-transitions

Transitions

.card {
  transition:
    transform 180ms var(--ease-out, cubic-bezier(0.2, 0, 0, 1)),
    box-shadow 180ms ease-out;
}
.card:hover { transform: translateY(-2px); }
  • Never write transition: all. It animates layout properties you did not intend, including ones changed by unrelated classes, and it makes theme switches crawl.
  • Durations: 100–200 ms for small UI feedback, 200–350 ms for panels and overlays, rarely above 500 ms. Exits ~20–30% shorter than entrances.
  • Prefer individual transform properties (translate, scale, rotate) when several rules contribute; they compose instead of overwriting one transform string.

Entry effects with @starting-style

Transitions do not run on first render or from display: none. @starting-style gives the “before” values; allow-discrete lets display and overlay flip at the right moment.

[popover], dialog {
  opacity: 1;
  translate: 0 0;
  transition:
    opacity 200ms ease-out,
    translate 200ms ease-out,
    display 200ms allow-discrete,
    overlay 200ms allow-discrete;
}
@starting-style {
  [popover]:popover-open, dialog[open] { opacity: 0; translate: 0 8px; }
}
[popover]:not(:popover-open), dialog:not([open]) { opacity: 0; translate: 0 8px; }

Unsupported browsers simply show the element without the fade; that is the intended fallback.

Animating to height: auto

OptionSupport noteUse when
interpolate-size: allow-keywords on :rootChromium only todayProgressive enhancement; others snap
grid-template-rows: 0fr → 1fr with an inner overflow: hidden wrapperBroadAccordions that must animate everywhere
<details> with ::details-contentChromium-ledNative disclosure where snapping is acceptable elsewhere
.collapse { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 250ms ease; }
.collapse[data-open="true"] { grid-template-rows: 1fr; }
.collapse > .inner { overflow: hidden; }

Keyframes

@keyframes spin { to { rotate: 360deg; } }
.spinner { animation: spin 0.9s linear infinite; }

@keyframes rise {
  from { opacity: 0; translate: 0 12px; }
}
.reveal { animation: rise 300ms ease-out both; animation-delay: calc(var(--i, 0) * 40ms); }
  • Omit to when the end state is the element’s normal style; the animation lands on it.
  • Use both (or backwards) fill mode when a delay would otherwise flash the final state.
  • Stagger with a --i custom property set inline (style="--i: 3") rather than nth-child chains.
  • To animate a custom property (gradient angle, color stop), register it first; unregistered custom properties flip discretely:
@property --angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
.ring { background: conic-gradient(from var(--angle), #6366f1, #22d3ee, #6366f1);
        animation: turn 4s linear infinite; }
@keyframes turn { to { --angle: 360deg; } }
  • linear() easing approximates springs and bounces in pure CSS; generate the point list with a tool rather than hand-writing it.

Scroll-driven animations

Supported in Chromium and in Safari 26; Firefox still keeps it behind a flag (on the Interop 2026 list). Always wrap in @supports so other browsers get the static end state.

@supports (animation-timeline: view()) {
  @keyframes fade-up { from { opacity: 0; translate: 0 24px; } }
  .reveal-on-scroll {
    animation: fade-up linear both;
    animation-timeline: view();
    animation-range: entry 10% cover 30%;
  }

  @keyframes grow { from { scale: 0 1; } }
  .progress-bar {
    position: fixed; inset: 0 0 auto; height: 3px; transform-origin: 0 50%;
    animation: grow linear both;
    animation-timeline: scroll(root block);
  }
}
  • Put animation-timeline after the animation shorthand; the shorthand resets it to auto.
  • Leave the time out of the shorthand: with a scroll or view timeline the duration resolves to auto. Early flagged implementations wanted an explicit one; 1ms is a harmless placeholder.
  • view() measures the element’s own box. If the element is transformed by the animation, animate a child or the measured range shifts while it moves.
  • Use named timelines (view-timeline-name: --card, timeline-scope) when the animated element is not the one being tracked.

Reduced motion

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

Use the global reset only as a safety net. Better: author motion inside @media (prefers-reduced-motion: no-preference) so the default is still, and replace large movement with an opacity fade instead of removing feedback entirely. Parallax, zooms and autoplaying loops longer than 5 seconds must stop or be pausable (WCAG 2.2.2, 2.3.3).

Performance

PropertyCost
transform, opacity, filter (most)Compositor only, cheap
background-color, color, box-shadowPaint every frame; fine for small elements
width, height, top, left, margin, grid-template-*Layout every frame; keep short and contained
  • will-change: transform only on elements about to animate, removed afterward. Applied to many elements it multiplies GPU memory and can make things slower.
  • Animating box-shadow on large cards: animate the opacity of a pseudo-element that already has the final shadow instead.
  • contain: layout paint on an animated widget keeps layout work from spreading.

Gotchas

  • Changing a class from display: none and adding a transition in the same frame shows no transition; use @starting-style or force a style flush.
  • animation on :hover restarts on every hover; a transition reverses smoothly instead.
  • transform on a parent creates a containing block: position: fixed children stop being viewport-fixed. Common cause of “modal jumps inside animated panel”.
  • Keyframes declared inside CSS Modules get hashed names; reference them from the same file.
  • steps() sprites need image-rendering and integer pixel sizes or they shimmer.
  • Safari needs the -webkit- prefix for backdrop-filter in older versions; animating it is expensive everywhere.

Verification

  • DevTools Performance panel: record the interaction; no long purple (layout) or green (paint) bars per frame on the animated element; frames stay near 16.7 ms.
  • Rendering tab: enable “Paint flashing” and “Layer borders”; only the intended layer repaints.
  • Emulate prefers-reduced-motion: reduce (Rendering tab) and confirm motion is removed or replaced, and the UI still communicates state.
  • Animations panel: slow to 10% to check easing, overlap and final position.
  • Test the unsupported path: in Firefox (scroll timelines) the content is visible and static.
  • If a Playwright suite exists, set reducedMotion: 'reduce' in the context to keep screenshots stable.

Output

When delivering, state: which elements animate, the properties used and why, duration/easing tokens, the reduced-motion behavior, and which features are progressive enhancement and what non-supporting browsers show.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "CSS Animations"
  short_description: "Smooth motion with zero JavaScript"
  default_prompt: "$css-animations Fade and slide this popover in when it opens, no JS library"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem