Super-AI-Components

Modality Rail

Vertical icon+label tool switcher for editors

What it is

The vertical, fixed-width tool switcher that runs down the left edge of an editor shell: one icon-over-label button per mode, a chevron that reveals whatever doesn't fit, and a settings/help group pinned below that never scrolls away.

Why it matters

It's the defining chrome of a studio shell — CapCut, Canva, Fotor, Simplified, Tripo, and Spline all build their editor around this exact column. Because the column is a fixed 92px, it can't grow taller to fit more tools, so overflow and pinning aren't edge cases — they're the two problems the component exists to solve.

Observed in: CapCut · Canva · Fotor · Simplified · Tripo · Spline

Anatomy

  1. 1modality-railRoot column, fixed at 92px, split into a scrollable middle and a pinned footer.
  2. 2modality-rail-scrollThe middle group of tool buttons. Never grows scroll styling of its own.
  3. 3modality-rail-itemOne icon-over-label button. aria-pressed carries the active state.
  4. 4modality-rail-badgeThe 'New' dot or crown/Pro mark riding on an item's icon.
  5. 5modality-rail-overflowThe chevron affordance that reveals items past maxVisible, not a scrollbar.
  6. 6modality-rail-pinnedSettings, plugins, help — a separate group below the middle, never inside it.

How to use it

Reach for it as the primary tool switcher in an editor shell, not a generic sidebar (that's sidebar-nav). Pass a flat `items` array for the scrollable middle and a separate `pinned` array for the handful of rows — settings, plugins, help — that must survive no matter how many tools the middle grows to. `maxVisible` controls how many items show before the rest collapse behind the overflow chevron; the component is controlled, so feed `activeId` back from `onSelect` the same way the switcher and sidebar-nav expect.

Do

  • Collapse extra tools behind the overflow chevron once the column runs out of room — the 92px width is fixed, not a scroll container.

  • Keep settings, plugins, and help in pinned, separate from items — they stay put no matter how the tool list grows.

  • Use the badge slot to advertise a feature the user hasn't bought — a 'New' dot or a Pro crown — and leave the row exactly as usable as any other.

Don't

  • Don't let the middle column scroll instead of using the chevron — a scrollbar in a 92px rail hides tools behind a sliver of a drag handle.

    Select
    Draw
    Text
    Layers
    Select
    Draw
    Text
    Layers
  • Don't render a badge as a bare colour dot with no accessible name — 'New' and 'Pro' both need to survive for screen-reader users, not just sighted ones.

    Audio

Accessibility

Keyboard
  • Each toggle group in the rail is a single tab stop with its own roving tabindex, so a rail with overflow and a pinned group is four stops in total: the visible tools, the chevron, the tools inside the popover once it is open, and the pinned group. Up and Down move within a group and wrap; they never carry you from the middle group into the pinned one.
  • Enter and Space activate a tool. Arrows only move the highlight, so someone can travel the whole column without switching tools.
  • The chevron is a plain button. Enter or Space opens the overflow popover, Escape closes it, and the list inside is navigated with Up and Down like any other group.
  • There is no `disabled` anywhere in this component. Every entry in `items` and `pinned` is always focusable and always activatable — gating a tool means leaving it out of the array, not marking it unavailable.
  • Activating the already-active tool reports an empty selection, which is discarded. The rail can never land on no tool at all.
Screen reader
  • All three toggle groups render as `role="group"` with `aria-orientation` explicitly suppressed — the attribute is not allowed on that role, so the value the primitive computes is an axe `aria-allowed-attr` failure whatever it says. None of the three groups carries a name of its own, so a screen reader meets three unlabelled groups: the tools announce fine, but the split between the scrolling middle and the pinned footer does not.
  • An item is a toggle button carrying `aria-pressed`, so the active tool is programmatic rather than a colour treatment. Its accessible name is the visible label plus, when `badge` is set, an sr-only " Pro" or " New" — the crown and the dot themselves live inside an `aria-hidden` span and contribute nothing.
  • The visible label is truncated to fit 92px, but the full string stays in the accessibility tree. A tool that renders as "Backgro…" is still announced in full.
  • The tooltip repeats the label rather than supplying it. Every item already has a real text name, so nothing is lost on the many occasions the tooltip never opens.
  • The overflow popup is `role="dialog"` named by `overflowLabel` — the same string the chevron announces, because the list has no heading to borrow a name from. Without it the popup would be an unnamed dialog, which fails outright.
  • Switching tools announces nothing beyond the pressed state of the button that was pressed. The canvas that just changed mode has to say so itself; the rail owns no live region.
Focus
  • Opening the overflow popover moves focus into the popup, and closing it returns focus to the chevron. Choosing a tool from that list closes the popover — so focus lands back on the chevron rather than on the tool that is now active, and that tool is inside a collapsed list where nothing shows it as pressed.
  • Items and the chevron both ship an explicit `focus-visible:ring-2`, so the rail is one of the few components here that does not depend on whatever global focus style you happen to have.

Watch out for

  • Relying on the tooltip as the only place an item's name appears. The label under the icon already carries it — the tooltip on hover is a supplement for the collapsed/tight case, never the sole source.
  • Marking the active tool with colour alone. modality-rail-item sets aria-pressed from the toggle-group's own state, so the active tool is programmatically discoverable even if a custom theme changes the colour treatment entirely.
  • Reaching for the badge slot to hide features instead of advertising them — like sidebar-nav's tier badge, a 'New' dot or crown mark on a rail item is the cheapest upsell the shell has precisely because the row stays fully usable underneath it.

Installation

npx shadcn@latest add https://super-ai-components.vercel.app/r/modality-rail.json

Requires a shadcn app on Base UI (style base-nova). Radix-based styles such as new-york fail to typecheck: these components use Base UI's render prop and callbacks like onOpenChangeComplete, which Radix primitives do not have.