ComponentsActions & navigationNav
On this page
Actions & navigation

Nav released

A list of links to an application's screens, each with an icon and a name, and the current one marked — at the side on a wide screen and in a drawer on a narrow one, wherever the application puts it.

  • Import@pacit/components/nav
  • Selectorsnav[pctNav], pct-nav-item, a[pctNavLink]

No APG pattern applies, and none is invented — the reasoning is under Accessibility ↓

nav.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctNav, PctNavItem, PctNavLink } from '@pacit/components/nav';
ExportedWhat it is
PctNavcomponentunreleasednav[pctNav]An application's navigation: a list of links to its screens, the current one marked.
PctNavItemcomponentunreleasedpct-nav-itemOne screen of the navigation: role="listitem", the step pct-crumb is for the breadcrumb.
PctNavLinkcomponentunreleaseda[pctNavLink]The navigation's link, on the consumer's own anchor: an icon, the screen's name, and the mark of the current screen when the anchor carries aria-current.
<nav pctNav aria-label="Screens">
  <pct-nav-item>
    <a pctNavLink routerLink="/rooms" routerLinkActive ariaCurrentWhenActive="page">Rooms</a>
  </pct-nav-item>
</nav>
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

API

What it reads, and what it writes back

Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.

unreleased marks 3 of them: on main, and not in @pacit/components@0.2.0 — the next release names the version.

An application's navigation: a list of links to its screens, the current one marked.

Inputs

NameTypeWhat it does
No inputs — the element is configured by its content.

Outputs

None of its own. The native events — (click), (focus), the form's submit — are the platform's, and two-way models above carry the rest.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-nav
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

In a test, PctNavHarness, PctNavItemHarness, PctNavLinkHarness from @pacit/components/testing read a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
list[data-pct-part="list"]the role="list" inside the landmark
icon[data-pct-part="icon"]the region of the link's icon, at its start
label[data-pct-part="label"]the region of the screen's name

Tokens

Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.

TokenStyles
--pct-nav-gap{pct.space.2}4pxGap between the links.
--pct-nav-icon-color{pct.text-muted}#334155 · #e2e8f0Colour of a link's icon.
--pct-nav-link-bg-current{pct.primary-100}#dbeafe · #1e40afBackground of the link to the current screen.
--pct-nav-link-bg-hover{pct.surface-100}#f1f5f9 · #1e293bBackground of a link under the pointer.
--pct-nav-link-fg{pct.text}#0f172a · #ffffffText colour of a link.
--pct-nav-link-fg-current{pct.on-primary-100}#1d4ed8 · #bfdbfeText colour of the link to the current screen.
--pct-nav-link-font-weight-current{pct.font.weight.medium}500Font weight of the link to the current screen.
--pct-nav-link-gap{pct.space.3}8pxGap between a link's icon and its name.
--pct-nav-link-height{pct.control.height.md}36pxMinimum height of a link.
--pct-nav-link-padding-x{pct.space.4}12pxInline padding inside a link.
--pct-nav-link-radius{pct.radius.md}8pxCorner radius of a link.
--pct-nav-marker-color{pct.primary}#2563eb · #60a5faColour of the bar at the start edge of the link to the current screen.
--pct-nav-marker-inset{pct.space.3}8pxHow far the bar marking the current screen stands in from the link's top and bottom.
--pct-nav-marker-width3pxWidth of the bar marking the current screen.

Theming

A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.

[data-theme='brand'] {
  --pct-nav-link-bg-current: #ccfbf1;
  --pct-nav-link-fg-current: #115e59;
  --pct-nav-marker-color: #0f766e;
}
Accessibility

Measured, not assumed

Pattern: none — the APG has no pattern for a site's navigation beyond the navigation landmark, which is the consumer's <nav>; inside it a role="list" and a listitem per link, the shape the APG's own examples write, and aria-current="page" on the current screen. Named in the class JSDoc.

Keyboard

keyeffecttest
Tab / Shift+Tabmoves from link to link, in the list's orderapps/sandbox-e2e/src/frame.spec.ts — "Tab walks the bar in its reading order, then the navigation"
Enteron a link, follows itthe same case; the following is the platform's

There are no handlers to map: every link is an anchor and answers the keyboard on its own. A roving tab stop would be the menu's pattern, and a navigation is not a menu.

What is measured, and what is not yet

Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.

MeasuredARIA pattern named in the class JSDoclibs/components/nav/src/nav.ts
MeasuredKeyboard map tested key by keythe table above — every row measured in apps/sandbox-e2e/src/frame.spec.ts
Measuredaxe audit on the component's own sandbox viewapps/sandbox-e2e/src/a11y.spec.ts — /frame in SBX_ROUTES
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — frame-app and frame-app-rtl
Measuredforced-colors: active — no state carried by colour alonelibs/components/nav/src/nav-link.scss — the current screen is a bar and a weight besides the tint; the bar Highlight, the links LinkText. The reading: apps/sandbox-e2e/src/forced-colors.spec.ts
n/aprefers-reduced-motion — duration from a tokennot applicable — the navigation moves nothing; the drawer it may stand in follows its own card
MeasuredTouch target ≥ 24×24 px outrightlibs/tokens/src/component.nav.json — a link's height is --pct-control-height-md, 36 px (32 px compact), over the whole width of the row
MeasuredSize axis aligned to --pct-control-height-*libs/tokens/src/component.nav.json — --pct-nav-link-height is the md control height
MeasuredDensity axislibs/tokens/src/component.nav.json — the link's padding and the gap inside it stand on space.3 and space.4, which the compact scope re-points, and its height on control.height.md, which it lowers; the gap between links stands on space.2, which the compact scope leaves
MeasuredRTL — no physical properties + a dir="rtl" screenshotlogical properties only, held by check-styles; the screenshot frame-app-rtl, where the bar of the current screen stands at the right
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /frame in SBX_ROUTES
n/aFormsnot applicable — a navigation holds no value
MeasuredParts registered in the inventorylibs/components/parts.snapshot.md — the three parts above
MeasuredTokens registered + an entry in contrast.policy.jsonlibs/tokens/src/contrast.policy.json — a link at rest, under the pointer and current (AA error), the bar of the current screen (UI error), the icon (UI warn); libs/tokens/tokens.snapshot.md
n/aStrings through PCT_TEXTSnot applicable — the component writes no word of its own
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./nav row
MeasuredScreen-reader logRead 2026-11-02 from docs/acr/at/ — three readers, whole sandbox. The landmark, its name, the count and the current screen: Orca on entering navigation · Screens · List with 4 items · Rooms · link., then Devices · link. · (Current page); NVDA Screens, navigation landmark on the first link — without the link's own name — then Devices, link, same page, current page. VoiceOver reached the bar's Sign out and lost the page before the navigation, so it has not read it
MeasuredA docs page with live examplesapps/sandbox/src/app/views/frame/ (the sandbox view). The published site: /components/nav, its demos' source in the code tab
MeasuredUnit + mutationlibs/components/nav/src/nav.spec.ts; libs/components/mutation.snapshot.md — nav.ts, measured by a narrow run
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

91.67%mutation score11 of 12 mutants killed
—e2e casescovered by the shared suites only
5colour pairs measuredlight and dark, at every token build
0visual baselinesChromium, the sandbox's own pictures
14elements in the preview3 listeners · 1 render to settle · 3729 µs in jsdom, held exactly but for the clock

Known limitations

  • No groups and no nesting. One flat list of screens; a heading between groups, or a screen with screens under it, is not drawn. A second nav[pctNav] under a heading of the application's is a group.
  • No rail. There is no narrow form of icons alone between the column and the drawer.
  • A forgotten aria-current is silent. A page with no current screen is legitimate, so nothing can warn about a link the consumer forgot to mark.
  • NVDA did not say the first link's name. On entering the navigation in the walk of 2026-11-02 NVDA read Screens, navigation landmark and stopped; the other links it read whole.
  • It does not close the drawer it stands in. A link pressed in a drawer leaves it open until the application closes it.