Skip to main content

ContextRail

Layout

A right-hand rail whose collapsed state is a 48px icon strip that doubles as its own section switcher — click a switcher entry to make it active, click the ACTIVE entry again to collapse/expand.

import { ContextRail } from "@elabs-ai/components-ui";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui ContextRail component (from @elabs-ai/components-ui) to this project.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Look ContextRail up before writing code: `npx -y @elabs-ai/components-cli docs ContextRail` prints its real props, variants, what it pairs with and what to avoid.
3. If @elabs-ai/components-ui is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Use it here: [the screen or file, and what it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Examples

5 live examples

Collapsed

`defaultOpen={false}` — the collapsed 48px icon strip, still fully operable as a switcher (uncontrolled, so the click-to-expand in the play function actually moves state).

Empty

No sections — the default localized empty state.

Narrow

Below `overlayBreakpoint` the rail mounts its own strip plus a `Sheet` instead of `Sidebar`. A viewport-size story parameter cannot force this branch under a headless run (the composed story's `window.innerWidth` doesn't track the addon's viewport there), so this story forces it deterministically with a very high `overlayBreakpoint` instead — reliable both interactively and under `vitest --project storybook`. Known constraint (task-11f-brief.md Finding 5): this story's own fixed `h-[560px]` host is shorter than the browser viewport, so the expanded body's `Sheet` (always viewport-height) visibly outruns the persistent strip below it — see the seam this reproduces in `ContextRail`'s own doc comment.

Many Sections

Eight sections in the default 20rem rail — the count at which the header's two occupants genuinely compete for the band. `sections` is caller-supplied and unbounded, and the switcher is a fixed-height row of 32px buttons, so left uncapped it reserves its full width before the heading gets any: the active section's label was measured down to roughly 20px of usable width (its own padding included), which is no label at all. The cap keeps the heading's half of the band and moves the overflow into the switcher, which scrolls.

API

ContextRail

PropTypeDescription
sectionsrequiredContextRailSection[]The sections the switcher can pick between. An empty array renders the `empty` slot instead of a switcher.
activeSectionIdstringControlled active section id.
defaultActiveSectionIdstringInitial active section id (uncontrolled). Defaults to `sections[0]`.
onActiveSectionChange(sectionId: string) => voidFires with the clicked section's id — including a re-click of the already-active section (open/close is a separate concern, see `open`).
openbooleanControlled expanded/collapsed state. Clicking the ACTIVE switcher entry toggles this; clicking an INACTIVE entry sets it `true`.
defaultOpenbooleanInitial expanded/collapsed state (uncontrolled). Defaults to `true`.
onOpenChange(open: boolean) => void
widthstringExpanded width, published as `--sidebar-width`. @default "20rem"
emptyReactNodeReplaces the default localized empty state when `sections` is empty.
overlayBreakpointnumberBelow this viewport width the rail renders its own 48px strip plus a `Sheet` for the expanded body instead of mounting `Sidebar` — dev-only warns if set below 768. @default 768

Also accepts: Omit<ComponentProps<"div">, "onSelect" | "children">