Navigation rail
A narrow column of icon destinations that switches one panel between modes. Reach for it when an app has a handful of permanent sections and the canvas is the product.
The rail
The panel the rail switches. One thing at a time, so nothing competes.
Code
<GdNavRail
v-model="mode"
:items="MODES"
label="Workbench sections"
controls="workbench-panel"
/>Counts and status
A badge holds a quantity. A dot holds a yes. Putting the word "on" in a badge is what produced the squashed lozenge this component was built to stop.
Code
const items = [
{ id: 'layers', label: 'Layers', icon: 'plane', badge: 12 },
{ id: 'engine', label: 'Engine', icon: 'materials',
status: true, statusLabel: 'Engine available' },
{ id: 'project', label: 'Project', icon: 'room', disabled: true }
]API
GdNavRail
Props
modelValuestring—The id of the current destination. `v-model` — the rail is a tablist, so selecting one is the model changing.
itemsGdRailDestination[][]The destinations. Omit and compose GdNavRailItems in the default slot instead — but that form has no keyboard, because the keyboard is wired to `items`.
labelstring"Sections"The accessible name for the rail. Required of any tablist; the default is a poor one, so pass something true.
controlsstring—The id of the panel the rail switches, written onto each item's `aria-controls`.
Slots
iconScoped, receives { item }. An app's own glyph set, without giving up the keyboard.
defaultComposed GdNavRailItems, for a rail that navigates rather than switches. No keyboard — see Usage.
GdNavRailItem
Props
labelrequiredstring—Shown under the glyph and used as the accessible name. One word — two wraps, and a wrapped label makes every item in the column a different height.
iconGdIconName—A glyph from the registry.
dstring—A 24×24 path, for a glyph the registry does not carry.
activebooleanfalseDraws the active indicator behind the glyph and lifts the label's ink and weight.
disabledbooleanfalsePresent but unreachable. Skipped by the arrow keys; still announced.
badgenumber—A COUNT, on the indicator's corner, drawn with GdBadge's `solid` tone. Not a place for a word — see Usage.
statusbooleanfalseA binary state, drawn as a dot. Use instead of a badge when the answer is yes/no rather than how many.
statusLabelstring—What the dot means, for a screen reader — "Engine available". Without it the dot announces the item's label plus "active".
Slots
defaultThe glyph, when neither `icon` nor `d` fits.
Usage Guidelines
- Use a rail when the destinations are few, permanent and known by their glyph. Four to six. Past that the labels stop being scannable and you want GdSideNav.
- Use
GdSideNavfor routes. A rail is a tablist: it switches a panel. A tab that changes the URL is a link wearing the wrong hat, and it loses middle-click and open-in-new-tab doing it. - A badge is a count; a dot is a state. "12 layers" is a number. "The engine is available" is a yes. Do not put a word in a badge — it is sized for a digit.
- One word per label. A label that wraps makes every item in the column a different height, and the column is the only thing holding the rail together.
- Disable, do not hide. A destination that vanishes tells the user nothing; one that is visibly inert tells them it is coming.
Behavior & Anatomy
The indicator sits behind the glyph, not the whole item
Painting the entire item — glyph, label and all — makes the current mode a solid block roughly three times the visual weight of its neighbours, so the rail reads as one heavy tile with grey text under it rather than as four peers of which one is current. The indicator is a pill sized to the glyph; the label sits outside it and changes ink and weight instead. Material 3's navigation rail makes the same split for the same reason.
Active is carried by a surface and ink and weight. Two greys apart is not a distinction everyone can make, and weight is the cue that survives greyscale.
A word will never fit a badge
The rail this component replaces put the string on in a 15px pill and anchored it inside the glyph's own box, so it squashed and clipped the icon underneath. Two characters plus padding do not fit a shape built for one digit, and no amount of min-width fixes a shape whose job is to hold a number. Both markers now hang off the indicator's corner, pulled out by half their own size, so the glyph stays whole.
The active fill is weak, and that is accounted for
The rail sits on --gd-surface-sunken, and against that ground the active indicator's fill measures 1.03:1 in the light and 1.74:1 in the dark. That does not fail WCAG 1.4.11: the criterion does not ask a fill to reach 3:1 when the state is identifiable by another means that does — and here two others are.
- The ink moves to
--gd-accent-ink: 8.01:1 in the light, 4.77:1 in the dark. - The label moves to semibold — the cue that survives greyscale, and the one colour vision cannot take away.
Everything else was measured on the same ground and clears its floor: the resting ink at 4.65 / 5.74, the hovered glyph at 9.95 / 9.31. Hover's own fill is faint by design — hover is not a state you must perceive to operate a control, and the ink is the real feedback.
The count badge is solid, not accent
GdBadge's default accent tone fills with --gd-accent-wash, which is the same value as --gd-surface-selected — so a count on an active rail item measured 1.00 against the thing behind it and read as one indigo blob. neutral fails the same way one surface over: its fill is --gd-surface-sunken, which is the rail's own ground.
A solid accent does not rescue it either — on the dark theme's selected surface it measures 2.04, under the 3:1 WCAG 1.4.11 asks of a graphic you must perceive to read a count. The solid tone uses --gd-surface-solid, the pair that genuinely inverts with the theme, and clears every ground in both: 16.10 and 16.61 in the light, 9.66 and 16.86 in the dark.
It is a tablist, and it delivers one
The rail switches a panel, which is what tabs are. So it takes the WAI-ARIA tabs contract — arrow keys, Home and End, and the roving tabindex that keeps Tab moving past the group rather than through every destination in it. That behaviour is useGdTabList, shared with GdTabs and GdChoiceList, so the three cannot drift.
88px, and the number is the argument
A rail costs one narrow column once and switches a single panel, where a second sidebar spends the canvas twice — and on a map or a workbench the canvas is the entire product. --gd-size-rail is 88px: enough for a 48px target — Material's floor, well clear of 2.5.8's 24 — with air either side, and enough that a one-word label never wraps. A label that wraps to two lines makes every item in the column a different height. Material 2 specified 72px and M3 moved to 80dp, so 88 is this system's call rather than a spec's.