Collapsible sidebar
Everything that works vertically works horizontally — pass direction="x" and the Collapsible animates grid-template-columns instead of rows. This demo is a miniature app frame: a header with a toggle, a nav rail, and a content pane sharing one flex row. Collapse the rail and watch the pane absorb the width over the same 500ms transition.
Dashboard
This pane is flex-1 — it absorbs whatever width the rail gives up, smoothly, for the whole 500ms.
The two load-bearing details
Horizontal collapse uses the same grid trick as vertical — the outer container animates a track between 0fr and 1fr — but width gets negotiated with the surrounding layout in a way height usually doesn't.
The rail sits in a flex row. The body of the frame is display: flex: the Collapsible is a flex item that's free to shrink, and the content pane is flex-1, so every pixel the rail releases flows straight into it. In a layout with rigid columns the track animation would still run, but nothing around it could move — the flex parent is what turns a width change into a layout change.
Nav items don't wrap. Mid-animation the rail's width passes through every value between expanded and zero. Labels allowed to wrap would re-break their lines at each step — a flicker of stacked fragments on every toggle. whitespace-nowrap on each item pins the text to a single line, and the container's built-in overflow: hidden clips it cleanly as the edge sweeps past. (A fixed width on the content achieves the same thing.)
Two smaller touches worth noticing: the rail starts open (useState(true)) with initialOpen set, so the first paint shows it expanded with no entry animation, and the header button carries aria-controls="sidebar-nav-rail" plus aria-expanded — the trigger contract holds no matter where the trigger lives or what it looks like.
import { Collapsible } from '@westopp/react-collapsible' import { useState } from 'react' const navItems = [ { label: 'Dashboard', current: true }, { label: 'Projects', current: false }, { label: 'Reports', current: false }, { label: 'Team', current: false }, { label: 'Settings', current: false }, ] export const SidebarDemo = () => { const [open, setOpen] = useState(true) return ( <div className="my-6 rounded-(--radius-callout) border border-(--color-line) bg-(--color-bg-elev) p-5"> <div className="overflow-hidden rounded-(--radius-control) border border-(--color-line) bg-(--color-bg)"> <div className="flex items-center gap-3 border-b border-(--color-line) px-4 py-3"> <button type="button" aria-controls="sidebar-nav-rail" aria-expanded={open} onClick={() => setOpen((prev) => !prev)} className="cursor-pointer rounded-full bg-(--color-accent) px-[18px] py-[8px] text-[14px] font-medium text-(--color-accent-ink)" > {open ? 'Hide nav' : 'Show nav'} </button> <span className="text-[14px] font-medium text-(--color-ink)">Acme Console</span> </div> <div className="flex items-stretch"> <Collapsible id="sidebar-nav-rail" open={open} direction="x" initialOpen className="border-r border-(--color-line)"> <nav aria-label="Demo sidebar" className="flex h-full flex-col gap-1 p-3"> {navItems.map((item) => ( <button key={item.label} type="button" className={ item.current ? 'cursor-pointer whitespace-nowrap rounded-(--radius-control) bg-(--color-line-soft) px-3 py-2 text-left text-[13.5px] font-medium text-(--color-ink)' : 'cursor-pointer whitespace-nowrap rounded-(--radius-control) px-3 py-2 text-left text-[13.5px] text-(--color-ink-soft)' } > {item.label} </button> ))} </nav> </Collapsible> <div className="min-w-0 flex-1 p-4"> <p className="text-[15px] font-semibold text-(--color-ink)">Dashboard</p> <p className="mt-2 text-[13.5px] leading-relaxed text-(--color-ink-soft)"> This pane is flex-1 — it absorbs whatever width the rail gives up, smoothly, for the whole 500ms. </p> <div className="mt-4 grid grid-cols-2 gap-3"> <div className="h-14 rounded-(--radius-control) border border-(--color-line-soft) bg-(--color-bg-elev)" /> <div className="h-14 rounded-(--radius-control) border border-(--color-line-soft) bg-(--color-bg-elev)" /> </div> </div> </div> </div> </div> ) }
What it covers
direction="x"— same component, same controlledopenprop, columns instead of rows.- Flex-driven layout — the rail shrinks, the
flex-1pane absorbs; no JavaScript ever measures a width. nowraplabels — text clips instead of reflowing while the rail moves.initialOpen— sidebars usually start open; this one renders expanded on first paint with no entry flicker.- Contained mock — the app frame lives inside the demo card; the same structure scales to a real shell where the flex row is your page body.
The wider set of direction="x" patterns and gotchas — fixed-width content, where the borders should live — is in Horizontal collapse. For the grid-track mechanics underneath it all, see How it works.