Skip to Content

Navigation

Navigation components inherit from BaseNavigation. They use the roving tabindex pattern: only one item in the group is tabbable at a time, and arrow keys move between items. States: idle, hover, focused, active, disabled.


Tabs

Base: BaseNavigation

States: idle, hover, focused, active, disabled

Dev-fill slots:

  • onTabChange — tab change handler (required)
  • announce.active — screen reader announcement for active tab (required)

Key behavior: Arrow Left/Right navigate between tabs. Home/End jump to first/last tab. Screen reader roles: tablist, tab, tabpanel. Active tab has aria-selected="true".

Validation: Tabs.Tab must be rendered inside a Tabs wrapper.

import { Tabs } from './kevlar'; <Tabs defaultValue="overview" onTabChange={(value, ctx) => { setActiveTab(value); }} announce={{ active: 'Tab selected' }} > <Tabs.List> <Tabs.Tab value="overview">Overview</Tabs.Tab> <Tabs.Tab value="settings">Settings</Tabs.Tab> <Tabs.Tab value="billing">Billing</Tabs.Tab> </Tabs.List> <Tabs.Panel value="overview">Overview content</Tabs.Panel> <Tabs.Panel value="settings">Settings content</Tabs.Panel> <Tabs.Panel value="billing">Billing content</Tabs.Panel> </Tabs>

Base: BaseNavigation

States: idle, hover, focused, active, disabled

Dev-fill slots:

  • Navigation items use BaseInteractive (each breadcrumb is a link/button)
  • separator — visual separator between items

Key behavior: Last breadcrumb is the current page (not a link, marked with aria-current="page"). All others are navigable links.

import { Breadcrumbs, Anchor } from './kevlar'; <Breadcrumbs> <Anchor href="/" onKevlarAction={async () => { router.push('/'); }} announce={{ loading: 'Navigating...', success: 'Navigated', error: 'Navigation failed' }}> Home </Anchor> <Anchor href="/products" onKevlarAction={async () => { router.push('/products'); }} announce={{ loading: 'Navigating...', success: 'Navigated', error: 'Navigation failed' }}> Products </Anchor> <span>Widget Pro</span> </Breadcrumbs>

Pagination

Base: BaseNavigation

States: idle, hover, focused, active, disabled

Dev-fill slots:

  • onKevlarAction — page change handler (required)
  • announce.active — screen reader announcement for active page (required)
  • total — total number of pages (required)

Key behavior: Arrow keys navigate between page buttons. Current page has aria-current="page". Ellipsis elements are not focusable.

import { Pagination } from './kevlar'; <Pagination total={20} onKevlarAction={async (ctx) => { await loadPage(ctx.value); }} announce={{ active: 'Page selected' }} />

Stepper

Base: BaseNavigation

States: idle, hover, focused, active, completed, disabled

Dev-fill slots:

  • onKevlarAction — step change handler (required)
  • announce.active — screen reader announcement for active step (required)

Key behavior: Steps can be completed, active, or disabled. Completed steps are navigable (click to go back). Disabled future steps are not focusable. Screen reader announces step number and status.

import { Stepper } from './kevlar'; <Stepper active={1} onKevlarAction={async (ctx) => { await goToStep(ctx.value); }} announce={{ active: 'Step activated' }} > <Stepper.Step label="Account" description="Create account" /> <Stepper.Step label="Profile" description="Fill profile" /> <Stepper.Step label="Review" description="Review and submit" /> </Stepper>

Base: BaseNavigation + BaseDisclosure (when has children)

States: idle, hover, focused, active, disabled

Dev-fill slots:

  • onKevlarAction — navigation handler (required)
  • announce.active — screen reader announcement (required)

Key behavior: When children are provided, acts as a disclosure (expand/collapse nested links). Arrow Right expands, Arrow Left collapses. Active link has aria-current="page".

import { NavLink } from './kevlar'; <NavLink label="Dashboard" onKevlarAction={async () => { router.push('/dashboard'); }} announce={{ active: 'Dashboard selected' }} /> {/* With nested links */} <NavLink label="Settings" announce={{ active: 'Settings expanded' }}> <NavLink label="General" onKevlarAction={async () => { router.push('/settings/general'); }} announce={{ active: 'General settings selected' }} /> <NavLink label="Security" onKevlarAction={async () => { router.push('/settings/security'); }} announce={{ active: 'Security settings selected' }} /> </NavLink>

TableOfContents

Base: BaseNavigation

States: idle, hover, focused, active, disabled

Dev-fill slots:

  • onKevlarAction — scroll-to-section handler (required)
  • announce.active — screen reader announcement (required)

Key behavior: Highlights the currently visible section as the user scrolls. Uses IntersectionObserver for scroll tracking. Arrow keys navigate between heading links.

import { TableOfContents } from './kevlar'; <TableOfContents links={[ { label: 'Introduction', link: '#intro', order: 1 }, { label: 'Setup', link: '#setup', order: 1 }, { label: 'Usage', link: '#usage', order: 1 }, ]} onKevlarAction={async (ctx) => { scrollToSection(ctx.value); }} announce={{ active: 'Section selected' }} />

Tree

Base: BaseNavigation + BaseDisclosure

States: idle, hover, focused, active, expanded, collapsed, disabled

Dev-fill slots:

  • onKevlarAction — node selection handler (required)
  • announce.active — screen reader announcement (required)

Key behavior: Tree navigation with role="tree", role="treeitem". Arrow Right expands a node, Arrow Left collapses or moves to parent. Arrow Up/Down move between visible nodes. Home/End jump to first/last visible node.

import { Tree } from './kevlar'; <Tree data={treeData} onKevlarAction={async (ctx) => { await selectNode(ctx.value); }} announce={{ active: 'Node selected' }} />

Anchor

Base: BaseInteractive (in navigation context) or BaseStatic (plain link)

States: idle, hover, focused, pressed, loading, success, error, disabled

Dev-fill slots:

  • onKevlarAction — navigation handler (required)
  • announce.loading / announce.success / announce.error (required)

Key behavior: Renders an a element with Kevlar interaction handling. For client-side navigation, use onKevlarAction to call router.push(). For external links, the action can be a no-op (the native href handles navigation).

import { Anchor } from './kevlar'; <Anchor href="/about" onKevlarAction={async () => { router.push('/about'); }} announce={{ loading: 'Navigating...', success: 'Navigated', error: 'Navigation failed' }} > About us </Anchor>

Burger

Base: BaseInteractive

States: idle, hover, focused, pressed, opened, closed, disabled

Dev-fill slots:

  • onKevlarAction — toggle handler (required)
  • announce.loading / announce.success / announce.error (required)
  • aria-label — describes the toggle action (e.g. “Toggle navigation”)

Key behavior: Hamburger menu toggle. Animates between open (X) and closed (three lines) states. Used with AppShell for mobile navigation toggle.

import { Burger } from './kevlar'; <Burger opened={navOpened} aria-label="Toggle navigation" onKevlarAction={async () => { toggleNav(); }} announce={{ loading: 'Toggling...', success: 'Navigation toggled', error: 'Toggle failed' }} />
Last updated on