react-collapsible/Examples/Collapsible sidebar
ExamplesHorizontal

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.

Acme Console

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.

TSXsidebar.tsx
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 controlled open prop, columns instead of rows.
  • Flex-driven layout — the rail shrinks, the flex-1 pane absorbs; no JavaScript ever measures a width.
  • nowrap labels — 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.