Collapse
Animate a panel’s height as it opens and closes, with support for mid-animation changes.
Use it for disclosure you build yourself: a filter panel, "show more", an inline error, a settings section. The content below moves with the height instead of jumping. For Base UI's Collapsible and Accordion, use the yj-collapse preset on the panel instead, or @youj/react's Collapsible and Accordion, which already do.
Installation
npm i @youj/motionFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Usage
The trigger owns the state. Give the panel an id, and point the trigger's aria-controls and aria-expanded at it.
import { useId, useState } from 'react'
import { Collapse } from '@youj/motion'
export function Filters() {
const id = useId()
const [open, setOpen] = useState(false)
return (
<>
<button type="button" aria-expanded={open} aria-controls={id} onClick={() => setOpen(!open)}>
Filters
</button>
<Collapse id={id} open={open}>
<FilterForm />
</Collapse>
</>
)
}Other attributes pass through to the panel, so it can carry a role, an aria-labelledby or a class.
Examples
Interrupting
The height eases to and from the content's own height, so there's nothing to measure or hard-code. Press Filters twice in quick succession: a reversal starts from the current height and heads back on the same curve, with no jump. Content added while the panel is open grows it normally, because at rest the height is auto.
The panel clips only while it moves or while it's closed. At rest nothing is clipped, so focus rings and popovers inside it aren't cut off.
One at a time
Drive several panels from one piece of state for an accordion. One panel closes while the next opens, on the same duration and curve.
Keeping state
By default the content unmounts once it has collapsed. With keepMounted, it stays in the DOM while closed, hidden and inert, so a half-filled form or a scroll position survives. duration sets the height's duration token, base by default. This section uses slow for a taller panel.
Reference
Propsopen, children, keepMounted… (4)
Props
| Prop | Type | |
|---|---|---|
openRequired | booleanWhether the content is shown. | – |
keepMounted | booleanKeep the content mounted while closed (hidden and inert), e.g. to preserve its state. default false | false |
duration | DurationToken'instant''fast''base''slow''deliberate'How long the height takes. default 'base' | 'base' |
childrenRequired | ReactNode | – |
Other props pass through: extends HTMLAttributes<HTMLDivElement>. | ||
MotionHeight, fade in, fade out · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Heightto and from auto, both ways, default duration | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Fade inthe content, alongside the height | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Fade outfaster than the collapse, so text never looks squashed | duration.fastease.exit140ms · cubic-bezier(0.4, 0, 1, 1) | 140mscubic-bezier(0.4, 0, 1, 1) |
Reduced motion
The panel opens and closes in one frame. The content below moves at once, and closed content is hidden and inert as usual.
Height is a layout animation, and here that's the point: the content below has to move. Keep panels to the size of a section. For whole pages, use a route transition.
Reduced motion
The panel shows and hides at once, with no height animation and no fade. What's shown and what's reachable are exactly the same as with motion.
AccessibilityARIA, focus
Accessibility
- Collapse adds no roles or state of its own. The trigger carries
aria-expandedandaria-controls, as in the examples. Addrole="region"andaria-labelledbywhen a panel is a landmark worth jumping to. - Closed content is
inertfrom the first frame of the close, so it can't take focus or clicks while it collapses. It then unmounts or, withkeepMounted, is hidden from assistive technology. - Keep the trigger in place while the panel moves. It sits outside the panel, so it never shifts under the pointer.