Tabs

Tabs switch between views that sit at the same level of one screen. Primary tabs sit at the top of the content, secondary tabs divide what is inside one of those views.

Usage

Each <Tabs> holds several <Tab> children. Set <Tabs> to the primary or the secondary variant.

Primary tabs

<script lang="ts">
	import { Tab, Tabs } from 'noph-ui'
</script>

<Tabs value="videos">
	<Tab badge badgeLabel="2" value="videos">Videos</Tab>
	<Tab value="theme">Theme</Tab>
	<Tab badge value="settings">Settings</Tab>
</Tabs>

Icons

<script lang="ts">
	import { Tab, Tabs } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'
</script>

<Tabs value="videos">
	<Tab badge badgeLabel="2" value="videos">
		{#snippet icon()}<Icon>videocam</Icon>{/snippet}Videos
	</Tab>
	<Tab value="theme">{#snippet icon()}<Icon>palette</Icon>{/snippet}Theme</Tab>
	<Tab badge value="settings">{#snippet icon()}<Icon>settings</Icon>{/snippet}Settings</Tab>
</Tabs>

Secondary tabs

<script lang="ts">
	import { Tab, Tabs } from 'noph-ui'
</script>

<Tabs variant="secondary" value="videos">
	<Tab badge badgeLabel="2" value="videos">Videos</Tab>
	<Tab value="theme">Theme</Tab>
	<Tab badge value="settings">Settings</Tab>
</Tabs>

Icons

<script lang="ts">
	import { Tab, Tabs } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'
</script>

<Tabs variant="secondary" value="videos">
	<Tab badge badgeLabel="2" value="videos">
		{#snippet icon()}<Icon>videocam</Icon>{/snippet}Videos
	</Tab>
	<Tab value="theme">{#snippet icon()}<Icon>palette</Icon>{/snippet}Theme</Tab>
	<Tab badge value="settings">{#snippet icon()}<Icon>settings</Icon>{/snippet}Settings</Tab>
</Tabs>

Scrollable tabs

By default the tabs share the full width equally. When the tabs or labels do not fit, set scrollable. Each tab is then as wide as its label and the strip scrolls sideways. The first tab sits 52dp from the leading edge, so the cut off tabs at the end show that there is more. Selecting a tab scrolls it towards the middle of the strip.

<script lang="ts">
	import { Tab, Tabs } from 'noph-ui'

	const breeds = [
		'Akita',
		'Alaskan Malamute',
		'Australian Shepherd',
		'Azawakh',
		'Barbet',
		'Basenji',
		'Beagle',
		'Bernese Mountain Dog',
	]

	let value = $state('Akita')
</script>

<div style="width:100%;max-width:400px">
	<Tabs bind:value scrollable aria-label="Dog breeds">
		{#each breeds as breed (breed)}
			<Tab value={breed}>{breed}</Tab>
		{/each}
	</Tabs>
</div>

Selection

The value of <Tabs> sets the selected tab: the <Tab> with the same value is selected. Each <Tab> needs a unique value within its <Tabs>. Use bind:value on <Tabs> to track the selected tab in your state.

Selected tab: videos
<script lang="ts">
	import { Tab, Tabs } from 'noph-ui'

	let value = $state('videos')
</script>

<div>
	<Tabs bind:value>
		<Tab value="videos">Videos</Tab>
		<Tab value="theme">Theme</Tab>
		<Tab value="settings">Settings</Tab>
	</Tabs>
	<div style="margin-top:1rem;font-size:0.825rem">Selected tab: {value}</div>
</div>

Give a <Tab> an href to render it as a link to another page or route. The value prop still tracks the selected tab across pages, which helps with SSR and deep linking. Clicking a link tab selects it at once, and Space follows the link like Enter.

<script lang="ts">
	import { page } from '$app/state'
	import { Tab, Tabs } from 'noph-ui'
</script>

<div>
	<Tabs variant="secondary" value={page.url.searchParams.get('tab') || 'videos'}>
		<Tab data-sveltekit-noscroll href="/components/tabs?tab=videos" value="videos">Videos</Tab>
		<Tab data-sveltekit-noscroll href="/components/tabs?tab=theme" value="theme">Theme</Tab>
		<Tab data-sveltekit-noscroll href="/components/tabs?tab=settings" value="settings">
			Settings
		</Tab>
	</Tabs>
</div>

Theming

Tabs are drawn from the color roles: --np-color-primary for the indicator and the selected label, --np-color-on-surface-variant for the rest, and --np-color-secondary for the focus ring.

Custom propertyDefault value
--np-tabs-indicator-radius--np-shape-corner-full on primary tabs, 0 on secondary tabs

Accessibility

The strip renders role="tablist" and each tab role="tab" with aria-selected. The tabs share one tab stop: the left and right arrow keys move between them, Home and End jump to the ends, Space and Enter select the focused tab, and Tab leaves the strip for the panel.

Where a tab reveals a panel on the same page, give the panel an id, point at it with controls and mark the panel role="tabpanel". Tabs that navigate take an href instead and stay real links. A badge on a tab is read after the tab's name: the count, or New notification for a dot. Pass badgeAriaLabel to say what the number counts.

API

Tabs

Attributes

AttributeTypeDefaultDescription
valuenumber | stringValue of the current tab.
variant'primary' | 'secondary''primary'The style of the tabs: 'primary' or 'secondary'.
scrollablebooleanfalseTabs as wide as their labels that scroll sideways, the first one 52dp from the leading edge. Without it the tabs share the width equally.
elementHTMLElementA reference to the root DOM element of the component.

Tab

Attributes

AttributeTypeDefaultDescription
inlineIconbooleanfalseOnly affects the primary variant.
badgebooleanfalse
badgeLabelstring | number | undefinedundefinedText shown inside the badge.
badgeAriaLabelstring | undefinedundefinedRead after the label, for example "2 new videos". Without it the count is read, or New notification for a dot.
controlsstring | undefinedundefinedThe id of the panel this tab controls, rendered as aria-controls.
valuenumber | stringundefinedIdentifies the tab. The tab is selected when it matches the value of <Tabs>.
iconSnippet | undefinedundefinedIcon shown before the label, as a snippet such as an <Icon>.
hrefstringundefinedRenders the tab as a link to this URL.
elementHTMLElementA reference to the root DOM element of the component.