Skip to content

SectionNav

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/section-nav.json
See it liveA settings column with icons and a current section, beside the heading it opens - press a row to move.→

A settings screen is the usual case: five or six sections, each its own address so it can be linked to and the back button walks between them, listed down the left with the current one tinted. Every product draws the same column, and every product draws the active row a little differently — which is exactly the drift a shared list exists to stop.

<div className="grid gap-8 md:grid-cols-[11rem_minmax(0,1fr)]">
<SectionNav
label={t('settings.title')}
items={SECTIONS.map((id) => ({ id, label: t(`settings.section.${id}`), icon: <Icon id={id} /> }))}
activeId={section}
render={(item) => <Link to={`/settings/${item.id}`} />}
className="md:sticky md:top-0"
/>
<div>
<SectionHeading title={t(`settings.section.${section}`)} description={t(`settings.hint.${section}`)} />
<Body section={section} />
</div>
</div>

The rows are your links. render takes the item and answers with the element to draw it as — a router’s link, an <a>, whatever you navigate with — and the component puts the row’s clothes, its icon and its aria-current on it, the way Button takes a render. Without render a row is a button and onSelect says which was pressed, for a screen whose sections are state rather than routes.

The current section is announced, not only tinted. The active row carries aria-current="page", which is how a screen reader learns which of six identical links is where you are.

The caption names the landmark. label is drawn above the list and the <nav> is labelled by it, so the same word serves the eye and the reader. labelHidden keeps the name and takes the caption off the screen — for a screen whose own title already says it, where a list captioned “Settings” under a heading “Settings” names the screen twice.

A long name stays on one line. The column is a fixed width, and a label that wraps makes one row taller than every other — which in a two-language product happens to one section and not the rest, so the column reads as ragged and the line height stops meaning anything. A label given as a string is the row’s title as well, so the whole name is there on hover.

A section can say what it holds. description is a line under the label — “Theme, language, start screen” — so a reader chooses a section by its contents rather than guessing from one word. It is cut at two lines, for the same reason the label is cut at one.

The heading is the other half. SectionHeading is the title and the one-line hint above a section’s body, so the column and the page it opens are set in the same type.

Props

Read from the types in ui/section-nav.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

SectionNav

PropTypeDefault
labelrequiredstringWhat the list is called: the caption above it, and the name a screen reader gives the landmark.
itemsrequiredreadonly SectionNavItem[]The sections, in order. Each row is a button, or the element render returns for it.
activeIdstringWhich item is the current page.
render(item: SectionNavItem) => RenderProp<Record<string, unknown>>The element a row is drawn as - render={(item) => <NavLink to={…} />}. The row's props are merged onto it, the way Button takes a render.
onSelect(id: string) => voidPressed, whatever the row is drawn as.
labelHiddenbooleanfalseKeep the caption as the landmark's name and take it off the screen. For a screen whose own title already says it - a settings screen headed "Settings" with a list captioned "Settings" names itself twice, which is what kilna's did.
level1 | 2 | 3 | 4 | 5 | 62Where the caption stands in the outline: 2 by default, beside the screen's title.

And every prop of React's HTMLAttributes<HTMLElement> except children, onSelect, passed on.

SectionHeading

The title of the open section and the line under it.

PropTypeDefault
titlerequiredReactNodeThe open section's title, drawn as a heading.
descriptionReactNodeA line under the title that says what the section is for.
level1 | 2 | 3 | 4 | 5 | 62Where the open section's title stands in the outline: 2 by default.

And every prop of React's HTMLAttributes<HTMLDivElement> except title, 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: 13 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/section-nav.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.38.0
    • feat a heading stands at the level the page gives it 101c183
  3. v0.36.0
    • fix set the notes in dim, which clears AA e9e9a0e
  4. v0.33.0
    • feat a collapsed rail, a centred title bar, described sections b91b8d6
  5. v0.32.0
    • fix let an icon keep the size it is given, across the set 217b558
  6. v0.29.1
    • fix keep a long section name on one line 5e1e695
  7. v0.26.0
    • feat add the section column and its heading 0241e1f