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. That makes it the only tab stop in the bar, so the whole bar is one stop in the tab order and the arrow keys move between destinations, which is what a screen reader user expects of a navigation row.

<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>

Give an item an href and it renders an <a>. A selected item gets aria-current="page" either way.

<NavigationBarItem
	label="Home"
	href="/"
	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. Give it a badgeAriaLabel so the count is announced, otherwise the badge is hidden from assistive technology.

<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"), which M3 recommends for compact, phone-width screens. Once the bar has room to spare, switch to arrangement="centered" so each item keeps its own content width and the group sits in the middle instead of stretching edge to edge.

<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 instead of above it. M3 pairs this with the centered arrangement once a bar has the width to spare.

<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" arrangement="centered">
		{#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 propertyDescription
--np-navigation-bar-container-colorBackground of the bar.
--np-navigation-bar-item-active-indicator-colorThe pill behind the selected icon.
--np-navigation-bar-item-font-weightLabel weight.
--np-navigation-bar-item-selected-font-weightLabel weight of the selected item.

API

Renders a <nav> and takes its attributes, so give it an aria-label when a page has more than one navigation landmark. bind:element gives you the <nav>.

AttributeTypeDefaultDescription
arrangement'equal-weight' | 'centered''equal-weight'Whether items stretch to share the bar equally, or keep their own width and group in the middle.

Renders a <button>, or an <a> with an href, and takes that element's attributes.

AttributeTypeDefaultDescription
labelstringrequiredDestination name, shown under the icon.
iconSnippetrequiredThe icon. A navigation bar item always has one.
selectedbooleanfalseMarks 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.
badgebooleanfalseShows a badge on the icon.
badgeLabelstring | number | undefinedundefinedBadge text. Without it the badge is a small dot.
badgeAriaLabelstring | undefinedundefinedAnnounces the badge. Without it the badge is hidden from assistive technology.