UNPKG

alouette

Version:

A modern, customizable design system built on top of NativeWind v5 with configurable defaults

250 lines (184 loc) 8.05 kB
--- name: alouette-navigation description: > Segmented navigation. NavBar + NavBarItem move between destinations (role="navigation" + role="link" + aria-current="page"); Tabs + Tab switch views on one screen (role="tablist" + role="tab" + aria-selected). Both are compose-children groups (no options array) sharing the RadioButtonGroup material: a lowered 44px bar with a raised chip per item, optional leading icon, accent + disabled, controlled value or defaultValue + onValueChange, and a per-item href + onPress (a real anchor on web, and what expo Router <Link asChild> injects) that wins over the group callback. Load when building a tab bar, a section switcher, or navigation between routes. type: core library: alouette library_version: "22.6.0" requires: - alouette-theming - alouette-actions sources: - "christophehurpeau/alouette:packages/alouette/src/ui/navigation/NavBar.tsx" - "christophehurpeau/alouette:packages/alouette/src/ui/navigation/NavBarItem.tsx" - "christophehurpeau/alouette:packages/alouette/src/ui/navigation/Tabs.tsx" - "christophehurpeau/alouette:packages/alouette/src/ui/navigation/Tab.tsx" - "christophehurpeau/alouette:packages/alouette/src/ui/selection/SelectionContext.tsx" - "christophehurpeau/alouette:packages/alouette/src/ui/selection/SegmentedBar.tsx" - "christophehurpeau/alouette:packages/alouette/src/ui/selection/SegmentedItem.tsx" - "christophehurpeau/alouette:packages/alouette/src/ui/navigation/NavBar.stories.tsx" - "christophehurpeau/alouette:packages/alouette/src/ui/navigation/Tabs.stories.tsx" --- This skill builds on alouette-theming. Read it first for the accent model. # alouette — Navigation Two segmented groups over one shared base (`src/ui/selection/`): a lowered 44px bar whose selected item raises a chip. `NavBar` navigates between destinations, `Tabs` switches views on the same screen. They differ only in accessibility semantics — pick by what the press does, not by how it looks. | | `NavBar` / `NavBarItem` | `Tabs` / `Tab` | | --- | --- | --- | | use for | routes, destinations | views on the current screen | | container role | `navigation` | `tablist` | | item role | `link` | `tab` | | selected marker | `aria-current="page"` | `aria-selected` | ## Setup ```tsx import { Tabs, Tab } from "alouette"; <Tabs aria-label="Period" defaultValue="week" onValueChange={setPeriod}> <Tab value="day" label="Day" /> <Tab value="week" label="Week" /> <Tab value="month" label="Month" /> </Tabs>; ``` ## Core Patterns ### Controlled vs uncontrolled Both groups own the value: `defaultValue` for uncontrolled, `value` + `onValueChange` for controlled. A `NavBar` backed by a router is controlled — its value is the current route, matched against each item's `href`. ```tsx <NavBar aria-label="Main" value={pathname} onValueChange={(href) => router.push(href)}> <NavBarItem href="/home" label="Home" /> <NavBarItem href="/reports" label="Reports" /> </NavBar> ``` `NavBarItem` has no `value`: `href` is its identity. On web it renders a real `<a href>` (native ignores it), and the item cancels the browser's own navigation — routing stays the app's job, through `onValueChange` or `onPress`. A disabled item drops its `href`, since a disabled pressable never sees the press that would cancel it. ### Per-item onPress, and expo Router links An item may navigate itself; its `onPress` then replaces the group's `onValueChange` for that item. The group must be controlled in that case — its internal value is never updated. A custom handler that routes must call `event.preventDefault()` on web, or the anchor reloads the page under it. `<Link asChild>` injects exactly those two props (`href` and a `preventDefault` ing `onPress`), so it composes without repeating the route: ```tsx import { Link } from "expo-router"; <NavBar aria-label="Main" value={pathname}> <Link href="/home" asChild> <NavBarItem label="Home" /> </Link> </NavBar>; ``` ### Leading icon `icon` takes a rendered icon element and is auto-sized and auto-tinted from the item's selected/disabled state. ```tsx import { HouseRegularIcon } from "alouette-icons/phosphor-icons/HouseRegularIcon"; <NavBarItem href="/home" label="Home" icon={<HouseRegularIcon />} />; ``` ### Accent and disabled `accent` themes the whole bar (the group wraps itself in the accent theme); `disabled` on the group disables every item, `disabled` on an item disables just that one. ```tsx <Tabs aria-label="Period" accent="brand" defaultValue="week"> <Tab value="week" label="Week" /> <Tab disabled value="month" label="Month" /> </Tabs> ``` ### Wiring tab panels `Tab` passes `id` and `aria-controls` through; render the panel yourself and point it back at the tab. `Tabs` renders no panel. ```tsx <Tabs aria-label="Ranges" value={range} onValueChange={setRange}> <Tab id="tab-week" aria-controls="panel-week" value="week" label="Week" /> </Tabs> <Surface role="tabpanel" id="panel-week" aria-labelledby="tab-week">…</Surface> ``` ## Common Mistakes ### HIGH Passing an options array instead of children Wrong: ```tsx <Tabs options={[{ value: "day", label: "Day" }]} /> ``` Correct: ```tsx <Tabs aria-label="Period" defaultValue="day"> <Tab value="day" label="Day" /> </Tabs> ``` These are compose-children groups, like `RadioButtonGroup`. There is no `options` prop; each child reads the selected value from the group's context. Source: packages/alouette/src/ui/navigation/Tabs.tsx ### HIGH Using Tabs for route navigation (or NavBar for in-page views) Wrong: ```tsx <Tabs value={pathname} onValueChange={router.push}>…</Tabs> ``` Correct: ```tsx <NavBar aria-label="Main" value={pathname} onValueChange={router.push}>…</NavBar> ``` The two render the same material but expose different semantics: `tab` promises a panel on the same screen, `link` + `aria-current="page"` promises a destination. Assistive tech announces them differently. Source: packages/alouette/src/ui/navigation/NavBar.tsx; ui/navigation/Tabs.tsx ### MEDIUM Item onPress on an uncontrolled group Wrong: ```tsx <NavBar aria-label="Main" defaultValue="/home"> <NavBarItem href="/settings" label="Settings" onPress={handlePress} /> </NavBar> ``` Correct: ```tsx <NavBar aria-label="Main" value={pathname}> <NavBarItem href="/settings" label="Settings" onPress={handlePress} /> </NavBar> ``` `onPress` replaces the group's selection callback, so the uncontrolled internal value stays where it was and the bar never moves its chip. Source: packages/alouette/src/ui/navigation/NavBarItem.tsx ### MEDIUM Navigating from an item onPress without preventDefault Wrong: ```tsx <NavBarItem href="/settings" label="Settings" onPress={() => router.push("/settings")} /> ``` Correct: ```tsx <Link href="/settings" asChild> <NavBarItem label="Settings" /> </Link> ``` An item with an `href` is a real anchor on web, so a handler that routes in JS must cancel the browser default or the page reloads on top of the JS navigation. The built-in handler does it; a custom one must too. Source: packages/alouette/src/ui/navigation/NavBarItem.tsx ### MEDIUM Hand-rolling the bar with PressableBox Wrong: ```tsx <Surface variant="lowered" className="flex-row"> <PressableBox variant="ghost">…</PressableBox> </Surface> ``` Correct: use `NavBar` / `Tabs`. The shared `SegmentedBar` / `SegmentedItem` already give the 44px tap target inside a 44px bar, the chip cross-fade, the `focus-visible` outline and the hover/active border — a hand-rolled bar drifts from all four. Source: packages/alouette/src/ui/selection/SegmentedBar.tsx; ui/selection/SegmentedItem.tsx ### LOW Omitting aria-label on the group `NavBar` renders a navigation landmark and `Tabs` a tab list; both should be named, especially when a screen has more than one. Source: packages/alouette/src/ui/navigation/NavBar.tsx See also: alouette-forms/SKILL.md for `RadioButtonGroup`, the same material with `radiogroup` semantics; alouette-icons/SKILL.md for importing icon elements.