GodUIGodUI
129Follow on X

Anatomy of the GodUI Accordion

How the GodUI Accordion looks exactly like a height animation (a clip edge sweeping over still text, with the rows riding it) while animating only translate and opacity.

01/05Why not animate height
tsx
/* shadcn/ui */@keyframes accordion-down {from { height: 0; }to { height: var(--radix-accordion-content-height); }}
01

Why not animate height

shadcn's accordion animates height from 0 to the panel's measured height. It looks right: the panel's bottom edge sweeps down over text that stays put, and everything below rides that edge. But every frame changes a size, so the browser recomputes layout for the panel and everything after it, then repaints, on the main thread. On a busy page those frames drop.

GodUI changes the height in one step and rebuilds the look of that sweep with transforms. A plain FLIP only gets you halfway, as above: the rows glide, but the panel just appears.

02/05A window that slides

tsx
// The box (overflow-hidden) slides down from under the trigger…panel.animate([{ translate: `0 ${from}px` }, { translate: `0 ${to}px` }],timing,);// …while its content slides up by exactly as much.content.animate([{ translate: `0 ${-from}px` }, { translate: `0 ${-to}px` }],timing,);
02

A window that slides

The panel's height snaps open at once, but its box starts tucked under the trigger, at translate: 0 -100%, and slides down. On its own that would drag the text down with it. So the content inside slides up by the same amount at the same instant, on the same curve.

The two offsets cancel, and the text never moves. What does move is the box's bottom edge, and since the box is overflow-hidden, that edge is a clip that uncovers the text line by line. That's the look of a height animation, made of two translates. Because they cancel for any curve, the spring can be anything, with no counter-scaling and no distortion.

03/05Rows ride the edge

tsx
const opening = [...changed.values()].includes(true);const ms = opening ? base : fast;// FLIP the rows before the snapped layout is painted…flushSync(() => flip(ms));// …then sweep each changed panel on the same clock.for (const [item, open] of changed) slidePanel(panelOf(item), open, timing);
03

Rows ride the edge

The rows below are FLIPped: measured before and after the snap, put back where they were with a translate, then played to rest. They start at the same offset as the panel's edge, on the same spring and duration, so at every frame the next row sits exactly on the clip edge. When one item opens while another closes, both edges and every row in between share one clock, so nothing ever gaps or overlaps.

A MutationObserver on the items' data-state fires in the same task as the click. flushSync runs the FLIP right there, before the browser paints the snapped layout, so you never see a frame of the jump. The accordion's own box gets the same treatment: in a parent that centers it, growing moves it up by half the new height, so it records where it was drawn on the click and glides from there too.

04/05Closing doesn't wait

tsx
<AccordionPrimitive.ContentclassName="pointer-events-none overflow-hidden …  data-[state=closed]:absolute data-[state=closed]:inset-x-0  data-[state=closed]:animate-godui-reveal-hold">
04

Closing doesn't wait

A closing panel can't shrink first and let the rows follow, because that is two steps. So the moment it closes it leaves the flow (absolute): the layout collapses at once, the rows below start rising, and the panel's edge sweeps back up with them. Radix would hide the panel immediately, so a near-silent godui-reveal-hold keyframe keeps it mounted until the sweep is done.

Closing runs on --godui-duration-fast, and the text fades out ahead of the edge: exits are quicker and softer than entrances. Opening, the content's blocks fade in a quarter-beat apart. Reverse in the middle and every piece (edge, text and rows) starts from where it's drawn.

05/05Result

Our flagship product combines cutting-edge technology with sleek design. Built with premium materials, it offers unparalleled performance and reliability.

Key features include advanced processing capabilities and an intuitive user interface designed for both beginners and experts.

05

The result

Open and close the items, and click again mid-sweep to reverse it.

What's animated

InteractionMechanismPropertiesEasingDuration
Openheight snaps; the panel's box slides from under the trigger while its content counter-slides (a clip edge sweeping over still text); rows below FLIP with the edgetranslateease-spring-smooth260ms
Open, contentblocks fade in, a quarter of --godui-duration-fast apart (up to 4)opacityease-out-expo260ms
Closethe panel leaves the flow at once; its edge sweeps up as the rows rise; text fades out ahead of ittranslate, opacityease-spring-smooth150ms
Open one, close anotherboth edges and every row share one clocktranslateease-spring-smooth260ms
Chevrontransition on transformrotate(180deg)ease-spring-smooth260ms open, 150ms close

The text inside a panel never moves: the box and its content travel the same distance in opposite directions, so only the clip edge does. The row below starts on that edge and stays on it every frame. Clicking again mid-sweep reverses from what's drawn. The dividers are each item's top border, so a line travels with its row. A nested accordion's change glides the outer rows too. In a parent that centers it (a preview stage, a dialog), the accordion moves when it grows; its own box glides to the new spot on the same clock instead of jumping.

Why GPU-only

The panel's box, its content and the rows below animate translate, the content's blocks animate opacity, and the chevron animates transform. Layout runs once per click. The chevron uses transform: rotate(180deg) rather than Tailwind's rotate-180 because Chrome won't composite the individual rotate property on an <svg>.

Reduced motion

Nothing moves: the height and the rows change in one step, and the panel's content only fades in or out (150ms). motion-reduce:transition-none stops the chevron's turn.

Replacing shadcn

npx shadcn add @godui/accordion overwrites components/ui/accordion.tsx. Exports, props and data-slot attributes match shadcn/ui new-york-v4, so existing imports keep working. It also installs godui-motion, which includes the useFlipGroup hook the accordion uses.

You can delete shadcn's accordion-down / accordion-up keyframes from your CSS; GodUI doesn't use them.