Navigation bar
Three to five top level destinations along the bottom of a compact screen. On a wider screen use a navigation rail instead.
Usage
Mark the current destination with selected. It becomes the bar's only tab stop, and
the arrow keys move between destinations.
<script lang="ts">
import { NavigationBar, NavigationBarItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
const destinations = [
{ value: 'home', label: 'Home', icon: 'home' },
{ value: 'search', label: 'Search', icon: 'search' },
{ value: 'mail', label: 'Mail', icon: 'mail' },
{ value: 'settings', label: 'Settings', icon: 'settings' },
]
let selected = $state('home')
</script>
<div style="width:100%;max-width:26rem">
<NavigationBar aria-label="Main">
{#each destinations as destination (destination.value)}
<NavigationBarItem
label={destination.label}
selected={selected === destination.value}
onclick={() => (selected = destination.value)}
>
{#snippet icon()}<Icon>{destination.icon}</Icon>{/snippet}
</NavigationBarItem>
{/each}
</NavigationBar>
</div>
As links
Give an item an href and it renders an <a>. A selected item gets aria-current="page" either way.
<NavigationBarItem
label="Home"
href={resolve('/')}
selected={page.url.pathname === '/'}
>
{#snippet icon()}<Icon>home</Icon>{/snippet}
</NavigationBarItem>Badges
badge puts a badge on the
icon. Without a badgeLabel it is a small dot. A screen reader reads it after the
destination's name: the count, or New notification for a dot. badgeAriaLabel replaces that text, to say what the number counts or to translate it.
<script lang="ts">
import { NavigationBar, NavigationBarItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<div style="width:100%;max-width:26rem">
<NavigationBar aria-label="Main with badges">
<NavigationBarItem label="Home" selected>
{#snippet icon()}<Icon>home</Icon>{/snippet}
</NavigationBarItem>
<NavigationBarItem label="Mail" badge badgeLabel={3} badgeAriaLabel="3 unread">
{#snippet icon()}<Icon>mail</Icon>{/snippet}
</NavigationBarItem>
<NavigationBarItem label="Search" badge badgeAriaLabel="New results">
{#snippet icon()}<Icon>search</Icon>{/snippet}
</NavigationBarItem>
</NavigationBar>
</div>
Labels
labelBehavior="selected" shows only the selected item's label. The hidden labels keep their
space, so the row does not shift as the selection moves.
<script lang="ts">
import { NavigationBar, NavigationBarItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
const destinations = [
{ value: 'home', label: 'Home', icon: 'home' },
{ value: 'search', label: 'Search', icon: 'search' },
{ value: 'mail', label: 'Mail', icon: 'mail' },
{ value: 'settings', label: 'Settings', icon: 'settings' },
]
let selectedCompact = $state('home')
</script>
<div style="width:100%;max-width:26rem">
<NavigationBar aria-label="Main, labels on selection">
{#each destinations as destination (destination.value)}
<NavigationBarItem
labelBehavior="selected"
label={destination.label}
selected={selectedCompact === destination.value}
onclick={() => (selectedCompact = destination.value)}
>
{#snippet icon()}<Icon>{destination.icon}</Icon>{/snippet}
</NavigationBarItem>
{/each}
</NavigationBar>
</div>
Arrangement
By default items share the bar equally (arrangement="equal-weight"), as M3 recommends
for phone-width screens. When the bar has room to spare, use arrangement="centered".
Each item then keeps its own width and the group sits in the middle.
<script lang="ts">
import { NavigationBar, NavigationBarItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
const destinations = [
{ value: 'home', label: 'Home', icon: 'home' },
{ value: 'search', label: 'Search', icon: 'search' },
{ value: 'mail', label: 'Mail', icon: 'mail' },
{ value: 'settings', label: 'Settings', icon: 'settings' },
]
let selectedCentered = $state('home')
</script>
<div style="width:100%;max-width:36rem">
<NavigationBar aria-label="Main, centered" arrangement="centered">
{#each destinations as destination (destination.value)}
<NavigationBarItem
label={destination.label}
selected={selectedCentered === destination.value}
onclick={() => (selectedCentered = destination.value)}
>
{#snippet icon()}<Icon>{destination.icon}</Icon>{/snippet}
</NavigationBarItem>
{/each}
</NavigationBar>
</div>
Horizontal items
orientation="horizontal" puts the icon beside the label, inside the active indicator.
M3 uses it on medium windows. Horizontal items always keep their own width and sit centred with
32dp between them, whatever the arrangement.
<script lang="ts">
import { NavigationBar, NavigationBarItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
const destinations = [
{ value: 'home', label: 'Home', icon: 'home' },
{ value: 'search', label: 'Search', icon: 'search' },
{ value: 'mail', label: 'Mail', icon: 'mail' },
{ value: 'settings', label: 'Settings', icon: 'settings' },
]
let selectedCentered = $state('home')
</script>
<div style="width:100%;max-width:36rem">
<NavigationBar aria-label="Main, horizontal items">
{#each destinations as destination (destination.value)}
<NavigationBarItem
orientation="horizontal"
label={destination.label}
selected={selectedCentered === destination.value}
onclick={() => (selectedCentered = destination.value)}
>
{#snippet icon()}<Icon>{destination.icon}</Icon>{/snippet}
</NavigationBarItem>
{/each}
</NavigationBar>
</div>
Theming
| Custom property | Description |
|---|---|
--np-navigation-bar-container-color | Background of the bar. |
--np-navigation-bar-item-active-indicator-color | The pill behind the selected icon. |
--np-navigation-bar-item-font-weight | Label weight. |
--np-navigation-bar-item-selected-font-weight | Label weight of the selected item. |
Accessibility
The bar renders a <nav> landmark. Give it an aria-label when the
page has more than one, so screen reader users can tell them apart. The selected item carries aria-current="page".
The items share one tab stop. Tab moves into the bar and out again. The left and right arrow keys
move between destinations, and Home and End jump to the ends. With href the items are links,
so middle click and open in new tab work. A badge is read after the item's name, as its description.
API
NavigationBar attributes
Renders a <nav> and takes its attributes, such as aria-label. bind:element gives you the <nav>.
| Attribute | Type | Default | Description |
|---|---|---|---|
arrangement | 'equal-weight' | 'centered' | 'equal-weight' | Whether vertical items stretch to share the bar equally, or keep their own width and group in the middle. Horizontal items are always centred. |
NavigationBarItem attributes
Renders a <button>, or an <a> with an href, and
takes that element's attributes.
| Attribute | Type | Default | Description |
|---|---|---|---|
label | string | required | Destination name, shown under the icon. |
icon | Snippet | required | The icon. A navigation bar item always has one. |
selected | boolean | false | Marks the current destination. Sets aria-current="page" and makes the item the bar's
tab stop. |
labelBehavior | 'always' | 'selected' | 'always' | Whether every label shows, or only the selected one. |
orientation | 'vertical' | 'horizontal' | 'vertical' | Whether the icon sits above the label, or beside it. |
badge | boolean | false | Shows a badge on the icon. |
badgeLabel | string | number | undefined | undefined | Badge text. Without it the badge is a small dot. |
badgeAriaLabel | string | undefined | undefined | Read after the label. Without it the count is read, or New notification for a dot. |