react-collapsible/Guides/Horizontal collapse
Guides~4 min

Horizontal collapse

One prop turns the collapse sideways: direction="x" animates width instead of height. The component itself doesn't change — same controlled open, same CSS Grid technique — but width brings two layout questions height never asked: what lets the container actually get narrower, and what stops the content from re-wrapping while it does.

What direction="x" changes

The animated track flips. Instead of grid-template-rows going 0fr to 1fr, the outer container animates grid-template-columns between the same two values — and because the inner wrapper has min-width: 0, the shrinking column track can crush the content's width the way the row track crushes its height. It's the technique from how it works, rotated ninety degrees: overflow: hidden clips, @starting-style covers entry, no JavaScript measures anything.

Everything else carries over unchanged — open stays controlled, and transitionDuration, hideMode, and conditional classes behave exactly as they do vertically.

Width needs a flex parent

Vertical collapse works anywhere because a div's height is content-driven: when the row track crushes the content, the box gets shorter and everything below moves up. Width doesn't work that way. In normal block flow a div stretches to its parent's full width regardless of what's inside it — the column track can animate to 0fr and the content disappears, but the box stays full-width until the transition ends and display: none snaps it out of layout. You get a vanishing act inside an empty strip, then a jump — not a collapse.

The fix is to let the container shrink-wrap its track. A flex row does exactly that: flex items size to their content by default, so as the track narrows, the container narrows with it and siblings reflow into the freed space. Put the Collapsible in a display: flex parent — the only layout change horizontal collapse typically needs.

TSXdrawer.tsx
import { useState } from 'react'
import { Collapsible } from '@westopp/react-collapsible'

const Drawer = () => {
const [open, setOpen] = useState(true)

return (
  // The flex row lets the Collapsible shrink-wrap its animating track.
  <div style={{ display: 'flex' }}>
    <Collapsible id="horizontal-collapse-drawer" open={open} direction="x">
      <nav style={{ whiteSpace: 'nowrap', padding: 16 }}>Drawer content</nav>
    </Collapsible>

    <main style={{ flex: 1, minWidth: 0 }}>
      <button
        type="button"
        aria-controls="horizontal-collapse-drawer"
        aria-expanded={open}
        onClick={() => setOpen((o) => !o)}
      >
        {open ? 'Hide drawer' : 'Show drawer'}
      </button>
    </main>
  </div>
)
}

Keep the content stable

During the animation the inner wrapper gets narrower frame by frame, and anything width-dependent recalculates at every one of those frames. Text is the worst offender: each frame it re-wraps into a narrower, taller column, so the panel shudders shut instead of sliding. Toggle the checkbox below and replay the animation to see the difference.

Watch this paragraph while the panel closes. Without a fixed width it re-wraps at every frame of the animation. With one, it holds still and slides out of view.

Content area — it grows as the panel gives up width.

Both fixes are the same idea — make the content's width independent of the track, so the track slides across stable content and the container's overflow: hidden clips it. Use white-space: nowrap for nav labels and short lines, or a fixed width for anything with real layout. The nowrap in the drawer sample above is doing exactly this job.

TSX
// Short lines: stop them wrapping, let the track clip them.
<nav style={{ whiteSpace: 'nowrap' }}>...</nav>

// Real layouts: pin the width so the panel slides over stable content.
<div style={{ width: 280 }}>...</div>

The sidebar pattern

Put it together and you have the collapsible rail: a flex row where the Collapsible is the sidebar and the main content grows with flex: 1. As the rail collapses, main absorbs the width continuously — the page reflows with the animation rather than snapping when it ends. initialOpen — alongside state that starts true — renders the rail open on first paint without playing the entry animation, and minWidth: 0 on main lets it shrink below its content's natural width when the rail reopens.

The main area grows with flex-1, so every pixel the rail gives up lands here — the page reflows with the animation instead of after it.

TSXapp-shell.tsx
import { useState } from 'react'
import { Collapsible } from '@westopp/react-collapsible'

const AppShell = () => {
const [open, setOpen] = useState(true)

return (
  <div style={{ display: 'flex', minHeight: '100vh' }}>
    <Collapsible id="horizontal-collapse-rail" open={open} direction="x" initialOpen>
      <nav style={{ height: '100%', whiteSpace: 'nowrap', padding: 16 }}>
        {/* nav links */}
      </nav>
    </Collapsible>

    <main style={{ flex: 1, minWidth: 0 }}>
      <button
        type="button"
        aria-controls="horizontal-collapse-rail"
        aria-expanded={open}
        onClick={() => setOpen((o) => !o)}
      >
        {open ? 'Hide navigation' : 'Show navigation'}
      </button>
      {/* page content */}
    </main>
  </div>
)
}