Skip to content
Components

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

layers

The panel the rail switches. One thing at a time, so nothing competes.

Four destinations, one currentArrow keys move between destinations, Home and End jump to the ends, and only the current item is in the tab order.
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.

A count, a status dot, and a disabled destinationLayers carries a number you check. Engine carries a state — available or not — which is a dot, not a word. Project is present but unreachable.
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

PropTypeDefault
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

icon

Scoped, receives { item }. An app's own glyph set, without giving up the keyboard.

default

Composed GdNavRailItems, for a rail that navigates rather than switches. No keyboard — see Usage.

GdNavRailItem

Props

PropTypeDefault
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.

activebooleanfalse

Draws the active indicator behind the glyph and lifts the label's ink and weight.

disabledbooleanfalse

Present 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.

statusbooleanfalse

A 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

default

The 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 GdSideNav for 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.

Navigate

Esc