Skip to content
Twenty One Media
webSeptember 7, 2026

The key Prop That Replays a CSS Animation

The question that shaped our homepage redesign: when someone lands and has never heard of us, what's the fastest way to show them what the software actually does?

Copy tells them. A working demo shows them.

We built a StudioWorkflow component with three examples a small business owner might recognize: drafting a quote from a new job request, writing a follow-up for a customer conversation that went quiet, and pulling a morning overview of where things stand. Each one shows the same three-part shape: something triggers the system, an agent does the work, and the owner gets a result ready to review. That structure is the actual product.

The component

The controls are a row of buttons, one per workflow. Clicking a button sets state and re-renders the example panel. Nothing fetches. The examples live as static data in the component because they're illustrative, not real.

const workflows = [
  { title: "Draft the quote", icon: FileText, ... },
  { title: "Keep the follow-up", icon: Mail, ... },
  { title: "See the whole picture", icon: BarChart3, ... },
];

export function StudioWorkflow() {
  const [active, setActive] = useState(0);
  const item = workflows[active];
  // ...
}

The animation problem

The workflow panel has a CSS entrance animation: a quick rise and fade. We wanted it to fire fresh every time a visitor switched examples, not just on first mount.

React's reconciliation normally keeps the same DOM node across re-renders and skips the animation entirely. The fix is one prop on the panel wrapper:

<div key={active} className="workflow-scene">

When active changes, React unmounts the old workflow-scene and mounts a new one from scratch. The CSS animation fires every time. No useEffect, no classList.add, no timeout to reset a class. The key is the entire mechanism.

Accessibility

The example panel carries aria-live="polite" and aria-atomic="true". When a visitor clicks a workflow button, screen readers announce the new content after finishing whatever they were already reading. aria-atomic="true" tells the reader to treat the whole panel as a single unit rather than announcing each changed node individually.

The buttons themselves use aria-pressed to indicate which workflow is active and aria-controls pointing at the live region's id. Content stays readable before JavaScript loads and on reduced-motion preferences, because the layout renders fully in HTML.

The spring easing

The rise animation uses a custom easing defined once in studio.css:

--studio-spring: linear(0, .104, .348, .62, .832, .958, 1.012, 1.024, 1.016, 1.006, 1);

That's a linear() approximation of a spring curve. It overshoots slightly, then settles. It reads more physical than ease-out and doesn't require a JavaScript animation library to achieve.

Why illustrative, not real

We could have pulled live data to show actual system activity. We didn't. The footer note in the component says why: "Built around your process, your data, and the decisions you want to keep."

Every business's quote workflow is different. A realistic-looking generic demo would feel either fabricated or irrelevant. A visitor sees "draft the quote" and maps it to their own version of that job. The specific data doesn't matter. The shape of the interaction does.

Three examples, one component, no backend. The key prop does the heavy lifting.