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
The headline prop is called headline. An optional subtitle sits under it, and the bar grows to fit rather than clipping.
<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 carries a search field instead
of a headline, as an emphasised, global 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, giving the screen's name
more weight. large is 120dp tall and medium 112dp; with a subtitle they grow
to fit it.
<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('')
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} placeholder="Search product">
<List aria-label="Suggestions">
{#each matches(query) as dish (dish)}
<ListItem onclick={() => (query = dish)}>{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.
<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.
<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 and fills with surface container once the content
scrolls under it, which separates it from the page without a shadow. Like collapsing, the fill
runs on animation-timeline: scroll() and follows the scroller set by scroller. In a browser without scroll-driven animations the bar stays flat. Set --np-app-bar-scrolled-container-color: transparent to keep the bar clear on scroll instead,
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 is driven by animation-timeline: scroll(), so there is no scroll listener and no state to keep. In
a browser without scroll-driven animations the bar simply 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.
A collapsing bar also needs scroll anchoring off on whichever element scrolls, otherwise the
browser compensates for the shrinking bar by pushing the scroll position back and the bar never
collapses. The component sets overflow-anchor: none on the scroller for you, so there is
nothing to add.
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 property | Description |
|---|---|
--np-app-bar-container-color | Background of the bar. |
--np-app-bar-scrolled-container-color | Background once the content scrolls under the bar. |
--np-app-bar-fill-distance | How far the content scrolls before the fill is complete. Defaults to 0.5rem. |
--np-app-bar-headline-color | Headline 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, which leaves the page free to keep its own <h1> where it belongs.
When the headline is the page's title, set headlineLevel to make it a heading of that level.
The medium and large variants carry a second copy of the title in the first row, the one that
fades in as the bar collapses. That copy is aria-hidden, so the title is announced
once. Everything you put in leading and trailing keeps its own name:
give each icon button a title.
API
AppBar attributes
Renders a <header> and takes its attributes. bind:element gives you that element.
| Attribute | Type | Default | Description |
|---|---|---|---|
headline | string | undefined | undefined | The screen's name. |
subtitle | string | undefined | undefined | Secondary line under the headline. |
headlineLevel | 1 | 2 | 3 | 4 | 5 | 6 | undefined | undefined | Renders 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. |
image | Snippet | undefined | undefined | An 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. |
search | Snippet | undefined | undefined | The search field, for variant="search". Replaces the headline, which a search
app bar does not show. |
leading | Snippet | undefined | undefined | Leading action, usually a navigation or back icon button. |
trailing | Snippet | undefined | undefined | Trailing actions. |
collapsible | boolean | false | Collapses a medium or large bar to one row on scroll. Ignored by the
one-row variants, which have nothing to collapse. |
scroller | 'root' | 'nearest' | 'root' | Which scroller the color fill and the collapse follow. |