SectionNav
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/section-nav.jsonA 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
| Prop | Type | Default | |
|---|---|---|---|
labelrequired | string | What the list is called: the caption above it, and the name a screen reader gives the landmark. | |
itemsrequired | readonly SectionNavItem[] | The sections, in order. Each row is a button, or the element render returns for it. | |
activeId | string | Which 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) => void | Pressed, whatever the row is drawn as. | |
labelHidden | boolean | false | Keep 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. |
level | 1 | 2 | 3 | 4 | 5 | 6 | 2 | Where 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.
| Prop | Type | Default | |
|---|---|---|---|
titlerequired | ReactNode | The open section's title, drawn as a heading. | |
description | ReactNode | A line under the title that says what the section is for. | |
level | 1 | 2 | 3 | 4 | 5 | 6 | 2 | Where 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.
- 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.38.0
- feat a heading stands at the level the page gives it
101c183
- feat a heading stands at the level the page gives it
- v0.36.0
- fix set the notes in dim, which clears AA
e9e9a0e
- fix set the notes in dim, which clears AA
- 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.1
- fix keep a long section name on one line
5e1e695
- fix keep a long section name on one line
- v0.26.0
- feat add the section column and its heading
0241e1f
- feat add the section column and its heading