NavRail
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/nav-rail.jsonOne list, three shapes, because they are the same list — and when they were three components in two products they drew the current entry three different ways.
<NavRail label={t('nav.screens')} items={SCREENS.map((id) => ({ id, label: t(`nav.${id}`), icon: <Icon id={id} /> }))} activeId={screen} render={(item) => <NavLink to={`/${item.id}`} />}/>The entries are your links. render takes an item and answers with the
element to draw it as — a router’s NavLink, a plain <a>, whatever you
navigate with — and the rail puts the clothes, the icon and the aria-current
on it, the way Button and
SectionNav take a render. That keeps the
router out of the registry: a set that imported one would install it in a
product that had chosen another. Without render an entry is a button and
onSelect says which was pressed.
The three shapes. column is the desktop rail, running the full height of
the window so its foot sits at the bottom because the nav reaches it. row is
a strip of tabs beside a product’s name in a header. bar is the phone’s,
along the bottom where the thumb already is — it carries the safe-area inset
for the home indicator, without which the last row of entries sits under it
with no way to scroll out.
A product usually has two of these on one page: a row at sm and up and a
bar below it. That is why label is required and is not drawn — it names
the landmark, so a reader meeting both is told which is which.
The current destination is announced, not only tinted. The active entry
carries aria-current="page". Colour says it to nobody who cannot see it.
A destination that is not built yet is not a link. soon draws the entry
greyed and renders it as a span, ignoring render — even a disabled button
is still in the tab order on some browsers, and tabbing onto a screen that
does not exist is a dead end. Put the version in end to say when it arrives.
Labels stay on one line. A rail is a fixed-width column, and a label that wraps makes one row taller than the rest — which in a two-language product happens to one entry and not the others.
Collapsed, the column keeps only its icons. A work open on screen wants the
width, and the destinations are still needed — so collapsed draws the column
as squares around the icons, at --spacing-rail-compact (56px), and each
entry’s name moves into a Tooltip beside it. The
tooltip’s trigger is the entry itself, so it opens on keyboard focus as well as
on hover, and the name stays in the entry off the screen: a reader hears
“Catalogue, current page” whether the rail is wide or narrow.
<AppShell sideWidth={collapsed ? 'var(--spacing-rail-compact)' : 'var(--spacing-rail)'} side={ <NavRail label="Screens" items={screens} activeId={screen} collapsed={collapsed} />}>The product passes the same collapsed to every NavRail and NavGroup it builds
its rail from — one boolean in the one place the rail is assembled. A collapsed
NavGroup is a short hairline, with its word kept for a reader, and an entry with
no icon shows its initial rather than an empty square. The row and bar
shapes have no narrow form: they are made of their names.
The narrow shapes clear the pointer target floor. A row tab and a bar
cell carry target-min, so they are at least 24 CSS pixels however small the
text is. A column entry is already taller than that.
Props
Read from the types in ui/nav-rail.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.
NavRail
| Prop | Type | Default | |
|---|---|---|---|
layout | column | row | bar | 'column' | |
labelrequired | string | What this set of destinations is called. Not drawn - it names the landmark, so a reader meeting two navs on one page can tell which is which. A product with a rail AND a bottom bar has exactly that. | |
itemsrequired | readonly NavRailItem[] | The destinations, in the order they are drawn. | |
activeId | string | Which destination is the open screen. | |
render | (item: NavRailItem) => RenderProp<Record<string, unknown>> | The element an entry is drawn as - render={(item) => <NavLink to={…} />}. | |
onSelect | (id: string) => void | Pressed, whatever the entry is drawn as. | |
collapsed | boolean | false | Icons only, with each name in a tooltip. The column shape only - a row of tabs and a bottom bar have no narrow form, because their names are what they are made of. |
And every prop of React's HTMLAttributes<HTMLElement> except children, onSelect, passed on.
NavGroup
The caption over a run of entries: Library, Team, Admin.
| Prop | Type | Default | |
|---|---|---|---|
collapsed | boolean | false | Draw a hairline instead of the caption, to match a collapsed rail; the words stay for a screen reader. |
And every prop of React's HTMLAttributes<HTMLDivElement>, passed on.
NavSpacer
Pushes everything after it to the far end of the rail.
Every prop of React's HTMLAttributes<HTMLDivElement>, passed on.
Accessibility
The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 12 rules applied and passed, and none failed. The accessibility report has every item.
History
Each release that changed ui/nav-rail.tsx - the file a product copies - newest first.
- v0.41.0
- docs read every props table from the component's types
ee3b159
- docs read every props table from the component's types
- v0.39.0
- feat let the frame go on paper, and keep a panel or a chart on one sheet
354d583
- feat let the frame go on paper, and keep a panel or a chart on one sheet
- v0.33.0
- feat a collapsed rail, a centred title bar, described sections
b91b8d6
- feat a collapsed rail, a centred title bar, described sections
- v0.32.0
- fix let an icon keep the size it is given, across the set
217b558
- fix let an icon keep the size it is given, across the set
- v0.29.0
- feat one list of destinations, in the three shapes it takes
a58c06d
- feat one list of destinations, in the three shapes it takes