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>

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

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

Renders a <nav> and takes its attributes, such as aria-label. bind:element gives you the <nav>.

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

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 | undefinedundefinedRead after the label. Without it the count is read, or New notification for a dot.