Skip to content

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/motion

import { Collapse } from '@youj/motion'

For 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.

Reference

Propsopen, children, keepMounted… (4)

Props

Collapse props
PropType
openRequiredboolean

Whether the content is shown.

–
keepMountedboolean

Keep the content mounted while closed (hidden and inert), e.g. to preserve its state.

default false

false
durationDurationToken
'instant''fast''base''slow''deliberate'

How long the height takes.

default 'base'

'base'
childrenRequiredReactNode–
Other props pass through: extends HTMLAttributes<HTMLDivElement>.
MotionHeight, fade in, fade out · reduced motion

Motion

Motion tokens
MomentTokens
Heightto and from auto, both ways, default duration
duration.baseease.standard

220ms · cubic-bezier(0.2, 0, 0, 1)

220mscubic-bezier(0.2, 0, 0, 1)
Fade inthe content, alongside the height
duration.baseease.standard

220ms · 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.exit

140ms · 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-expanded and aria-controls, as in the examples. Add role="region" and aria-labelledby when a panel is a landmark worth jumping to.
  • Closed content is inert from the first frame of the close, so it can't take focus or clicks while it collapses. It then unmounts or, with keepMounted, 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.