App bar

The bar across the top of a screen, holding the screen's name and its most important actions. It sticks to the top as the page scrolls.

Usage

Set the screen's name with headline. An optional subtitle sits under it, and the bar grows to fit it.

Headline
Subtitle
<script lang="ts">
	import { AppBar, IconButton } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'
</script>

<div style="width:26rem;max-width:100%;border-radius:1rem;overflow:hidden">
	<AppBar headline="Headline" subtitle="Subtitle">
		{#snippet leading()}
			<IconButton title="Back"><Icon>arrow_back</Icon></IconButton>
		{/snippet}
		{#snippet trailing()}
			<IconButton title="Search"><Icon>search</Icon></IconButton>
			<IconButton title="Choose date"><Icon>calendar_today</Icon></IconButton>
		{/snippet}
	</AppBar>
</div>

Variants

search shows a search field instead of a headline, as a prominent, app-wide entry point to search. Put the field in the search snippet. small is one row tall. medium and large put the headline on a second line below the actions, which gives the screen's name more weight. large is 120dp tall and medium 112dp. With a subtitle they grow to fit it.

Headline
small
Headline
medium
Headline
large
<script lang="ts">
	import { AppBar, IconButton, List, ListItem, Search } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'

	const dishes = ['Simple Classic Tacos', 'Mexican street corn', 'Chilaquiles verdes']

	let query = $state('')
	let expanded = $state(false)

	const matches = (q: string) =>
		q ? dishes.filter((d) => d.toLowerCase().includes(q.toLowerCase())) : dishes
</script>

<div style="display:grid;gap:1rem;width:26rem;max-width:100%">
	<div style="border-radius:1rem">
		<AppBar variant="search">
			{#snippet leading()}
				<IconButton title="Open navigation"><Icon>menu</Icon></IconButton>
			{/snippet}
			{#snippet search()}
				<Search bind:value={query} bind:expanded placeholder="Search product">
					<List aria-label="Suggestions">
						{#each matches(query) as dish (dish)}
							<ListItem
								onclick={() => {
									query = dish
									expanded = false
								}}
							>
								{dish}
							</ListItem>
						{/each}
					</List>
				</Search>
			{/snippet}
		</AppBar>
	</div>
	{#each ['small', 'medium', 'large'] as const as variant (variant)}
		<div style="border-radius:1rem;overflow:hidden">
			<AppBar {variant} headline="Headline" subtitle={variant}>
				{#snippet leading()}
					<IconButton title="Back"><Icon>arrow_back</Icon></IconButton>
				{/snippet}
				{#snippet trailing()}
					<IconButton title="Search"><Icon>search</Icon></IconButton>
					<IconButton title="Choose date"><Icon>calendar_today</Icon></IconButton>
				{/snippet}
			</AppBar>
		</div>
	{/each}
</div>

In a search app bar the field fills the space between the leading and trailing elements until it is 312dp wide. Beyond that it takes only half of the space, centred.

Centered

alignment="center" centres the headline and subtitle. In a small app bar they sit in the middle of the bar, whatever is beside them. In medium and large the second row is centred. In a search app bar the placeholder is centred.

Headline
Subtitle
Headline
Subtitle
<script lang="ts">
	import { AppBar, IconButton, Search } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'
</script>

<div style="display:grid;gap:1rem;width:26rem;max-width:100%">
	<div style="border-radius:1rem">
		<AppBar variant="search" alignment="center">
			{#snippet leading()}
				<IconButton title="Open navigation"><Icon>menu</Icon></IconButton>
			{/snippet}
			{#snippet search()}
				<Search placeholder="Search product" />
			{/snippet}
		</AppBar>
	</div>
	{#each ['small', 'medium'] as const as variant (variant)}
		<div style="border-radius:1rem;overflow:hidden">
			<AppBar {variant} alignment="center" headline="Headline" subtitle="Subtitle">
				{#snippet leading()}
					<IconButton title="Back"><Icon>arrow_back</Icon></IconButton>
				{/snippet}
				{#snippet trailing()}
					<IconButton title="Search"><Icon>search</Icon></IconButton>
				{/snippet}
			</AppBar>
		</div>
	{/each}
</div>

Image

The image snippet adds an image or logo. In a small app bar it replaces the headline, so give the image an alt text that names the screen. In medium and large it sits above the headline.

Noph UI
Noph UI
Headline
<script lang="ts">
	import { AppBar, IconButton } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'
</script>

{#snippet logo()}
	<img src="/favicon.svg" alt="Noph UI" width="32" height="32" />
{/snippet}

<div style="display:grid;gap:1rem;width:26rem;max-width:100%">
	{#each ['small', 'medium'] as const as variant (variant)}
		<div style="border-radius:1rem;overflow:hidden">
			<AppBar {variant} headline="Headline" image={logo}>
				{#snippet leading()}
					<IconButton title="Back"><Icon>arrow_back</Icon></IconButton>
				{/snippet}
				{#snippet trailing()}
					<IconButton title="Bookmark"><Icon>bookmark</Icon></IconButton>
				{/snippet}
			</AppBar>
		</div>
	{/each}
</div>

Scrolling

The bar starts in the page's surface color. Once the content scrolls under it, it fills with surface container instead of casting a shadow. The fill uses animation-timeline: scroll() and follows the scroller set by scroller. In a browser without scroll-driven animations the bar stays flat. To keep the bar clear on scroll, set --np-app-bar-scrolled-container-color: transparent and give its icon buttons a filled container.

Collapsing

collapsible shrinks a medium or large bar down to one row as the page scrolls, moving the headline up into the action row. It uses animation-timeline: scroll(). In a browser without scroll-driven animations the bar stays expanded.

By default it follows the page scroller. If the bar sits inside its own scroll container, pass scroller="nearest" and make the bar a direct child of that container.

The component sets overflow-anchor: none on the scroller, so the scroll position does not jump back while the bar shrinks.

Headline
Subtitle

Row 1

Row 2

Row 3

Row 4

Row 5

Row 6

Row 7

Row 8

Row 9

Row 10

Row 11

Row 12

Row 13

Row 14

Row 15

Row 16

Row 17

Row 18

Row 19

Row 20

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

<div style="width:26rem;max-width:100%;height:20rem;overflow:auto;border-radius:1rem">
	<AppBar variant="large" collapsible scroller="nearest" headline="Headline" subtitle="Subtitle">
		{#snippet leading()}
			<IconButton title="Open navigation"><Icon>menu</Icon></IconButton>
		{/snippet}
	</AppBar>
	<div style="padding:1rem;display:grid;gap:1rem">
		{#each Array.from({ length: 20 }, (_, i) => i) as row (row)}
			<p style="margin:0">Row {row + 1}</p>
		{/each}
	</div>
</div>

Theming

Custom propertyDescription
--np-app-bar-container-colorBackground of the bar.
--np-app-bar-scrolled-container-colorBackground once the content scrolls under the bar.
--np-app-bar-fill-distanceHow far the content scrolls before the fill is complete. Defaults to 0.5rem.
--np-app-bar-headline-colorHeadline text color.

Accessibility

The bar renders a <header>. At the top level of a page that makes it the banner landmark, so keep it out of <main>. By default the headline is styled text, not a heading, so the page can keep its own <h1>. When the headline is the page's title, set headlineLevel to make it a heading of that level.

The medium and large variants have a second copy of the title in the first row, which fades in as the bar collapses. That copy is aria-hidden, so the title is announced once. Give each icon button in leading and trailing a title.

API

AppBar attributes

Renders a <header> and takes its attributes. bind:element gives you that element.

AttributeTypeDefaultDescription
headlinestring | undefinedundefinedThe screen's name.
subtitlestring | undefinedundefinedSecondary line under the headline.
headlineLevel1 | 2 | 3 | 4 | 5 | 6 | undefinedundefinedRenders the headline as a heading of this level. Without it the headline is plain text.
alignment'start' | 'center''start'Headline and subtitle at the leading edge or centred.
imageSnippet | undefinedundefinedAn image or logo. It replaces the headline of a small app bar and sits above it otherwise.
variant'search' | 'small' | 'medium' | 'large''small'One row, or two with the headline below.
searchSnippet | undefinedundefinedThe search field, for variant="search". Replaces the headline.
leadingSnippet | undefinedundefinedLeading action, usually a navigation or back icon button.
trailingSnippet | undefinedundefinedTrailing actions.
collapsiblebooleanfalseCollapses a medium or large bar to one row on scroll. Ignored by the one-row variants.
scroller'root' | 'nearest''root'Which scroller the color fill and the collapse follow.