Skip to content

App Bar

The bar across the top of a screen: what leads it, its title, and what follows it. Renders a <header>.

app-bar/basic.tsx

Inbox

Ada Lovelace

There is no Base UI primitive underneath and no keyboard contract of its own. A header is a landmark, not a widget — Tab walks its controls one by one the way it walks the rest of the page, and that is correct. For a strip of related controls that should be one tab stop, put a Toolbar inside it.

What earns it a component is the scroll relationship. position decides whether the bar stays, and once it stays the bar measures the page rather than the other way round: data-scrolled while content is under it, data-hidden while a downward scroll has tucked it away. The styles read those two attributes — for the elevation and the hide — and so can yours, from plain CSS or a Tailwind arbitrary variant like data-[scrolled]:shadow-2.

Import

import { AppBar } from "@forte-ui/react";

AppBar is a namespace of four parts. Root is the surface and the grid; the other three are plain <div>s that place themselves in it, so any subset in any order still lays out — a bar with only a title, a bar with only a trailing cluster, a bar whose trailing slot is written first.

<AppBar.Root position="sticky" elevateOnScroll>
  <AppBar.Leading>
    <Button variant="ghost" iconOnly aria-label="Open navigation">…</Button>
  </AppBar.Leading>
  <AppBar.Title>Inbox</AppBar.Title>
  <AppBar.Trailing>
    <ThemeToggle />
    <Avatar.Root>…</Avatar.Root>
  </AppBar.Trailing>
</AppBar.Root>

The slots are slots, not controls: whatever goes in them is a real component with its own props, and wrapping one would mean re-exposing all of them. Compose a Button, a NavigationMenu, an Avatar; the bar only decides where they sit.

Examples

Variants

Four levels of chrome. plain is a bare row; panel fills with the panel colour; outline sits on the page background with a hairline underneath; frosted is outline made translucent, with the content behind it blurred.

app-bar/variants.tsx
plain
panel
outline
frosted

frosted is an enhancement in two directions, and the bar never depends on either. Where backdrop-filter is unsupported the bar is simply translucent. For a reader who has asked the OS to reduce transparency, it is opaque: the global glass alpha the fill is mixed at is already raised to 1 under that preference, and the blur sits behind the same query. prefers-contrast: more gets the opaque bar too.

Tones

tone is the second axis — which colour set the bar draws from — and it is independent of variant: a panel bar in primary is filled with the accent, an outline one sits on the accent's soft step.

app-bar/tones.tsx
Projects · neutral
Projects · primary
Projects · secondary

Nothing inside the bar is told the tone. A coloured bar re-points the foreground tokens for its subtree--forte-color-foreground, -foreground-muted, -panel-hover, -panel-active and the two border tokens — so the ghost buttons, the muted caption and the separator you compose into it recolour themselves for contrast, without a prop. Popups opened from the bar are portaled to <body> and keep the page's palette, which is what you want from a menu.

Sizes

Each step is the matching control height plus the bar's padding, so a size="sm" bar fits size="sm" buttons exactly and the title steps with it. The bar's size sets the bar, not its contents — the controls keep their own size prop, matched here by hand.

app-bar/sizes.tsx
Settings
Settings
Settings

Each height is a global density token — --forte-app-bar-h-sm, -md, -lg — so data-forte-density already drives it and a compact app gets a shorter bar without touching the component. The tokens are global on purpose: a sticky sidebar's offset or a page's scroll-padding-top can read the bar's height without knowing how it is built, which is exactly what this site's own shell does.

Sticky, and elevating on scroll

position="sticky" pins the bar to the top of its scroll container once the page reaches it — the nearest ancestor that scrolls, which is the window for a page bar and the box for a bar at the top of a ScrollArea, a dialog body or the demo below. It needs no layout compensation: the bar keeps its place in the flow.

elevateOnScroll is the Material "on scroll" behaviour: at the top the bar is part of the page, with no fill and no edge, and once content is under it, it is a surface over the page — the variant's fill and hairline, plus a shadow.

app-bar/sticky.tsx
Release notes

At the top the bar is part of the page — no fill, no edge. Scroll and it becomes a surface over the content, frosted, with the hairline and a shadow. Scroll back and it dissolves again.

At the top the bar is part of the page — no fill, no edge. Scroll and it becomes a surface over the content, frosted, with the hairline and a shadow. Scroll back and it dissolves again.

At the top the bar is part of the page — no fill, no edge. Scroll and it becomes a surface over the content, frosted, with the hairline and a shadow. Scroll back and it dissolves again.

At the top the bar is part of the page — no fill, no edge. Scroll and it becomes a surface over the content, frosted, with the hairline and a shadow. Scroll back and it dissolves again.

At the top the bar is part of the page — no fill, no edge. Scroll and it becomes a surface over the content, frosted, with the hairline and a shadow. Scroll back and it dissolves again.

At the top the bar is part of the page — no fill, no edge. Scroll and it becomes a surface over the content, frosted, with the hairline and a shadow. Scroll back and it dissolves again.

At the top the bar is part of the page — no fill, no edge. Scroll and it becomes a surface over the content, frosted, with the hairline and a shadow. Scroll back and it dissolves again.

At the top the bar is part of the page — no fill, no edge. Scroll and it becomes a surface over the content, frosted, with the hairline and a shadow. Scroll back and it dissolves again.

Only a neutral bar drops its fill at rest. On a coloured tone the fill is what carries the text's contrast, so there the surface stays and only the shadow is deferred. And plain has no surface to reveal, so pair the prop with one of the other three.

By default the bar elevates on the first pixel of scroll. scrollThreshold holds it in its at-rest state for longer — useful under a tall hero, where a shadow appearing the moment a finger moves reads as a twitch rather than a state change:

<AppBar.Root position="sticky" elevateOnScroll scrollThreshold={80}>

The prop moves data-scrolled rather than the elevation, so it is not tied to elevateOnScroll: a bar that only reports the attribute, with a border or a background of your own hung off [data-scrolled], gets the same delay. hideOnScroll is the exception — data-hidden follows the scroll's direction, not how far it has gone, so the threshold leaves it alone.

position="fixed" is also available, for a bar that is not the first thing in its scroll container. It leaves the flow entirely, so the content under it must add its own top padding — the bar's height is the --forte-app-bar-h-* token for its size, hairline included. Reach for sticky first; it is the same pinning without the padding.

Hiding on scroll

hideOnScroll slides the bar out of view while the page scrolls down and back in as soon as it scrolls up, by any amount. It never hides within its own height of the top, and a control inside it that holds focus keeps it on screen — Tab into a hidden bar and it returns.

app-bar/hide-on-scroll.tsx
Article

Scroll down and the bar slides away to give the text the room; scroll up by any amount and it is back. It never hides within its own height of the top, and Tab into it brings it back too.

Scroll down and the bar slides away to give the text the room; scroll up by any amount and it is back. It never hides within its own height of the top, and Tab into it brings it back too.

Scroll down and the bar slides away to give the text the room; scroll up by any amount and it is back. It never hides within its own height of the top, and Tab into it brings it back too.

Scroll down and the bar slides away to give the text the room; scroll up by any amount and it is back. It never hides within its own height of the top, and Tab into it brings it back too.

Scroll down and the bar slides away to give the text the room; scroll up by any amount and it is back. It never hides within its own height of the top, and Tab into it brings it back too.

Scroll down and the bar slides away to give the text the room; scroll up by any amount and it is back. It never hides within its own height of the top, and Tab into it brings it back too.

Scroll down and the bar slides away to give the text the room; scroll up by any amount and it is back. It never hides within its own height of the top, and Tab into it brings it back too.

Scroll down and the bar slides away to give the text the room; scroll up by any amount and it is back. It never hides within its own height of the top, and Tab into it brings it back too.

Scroll down and the bar slides away to give the text the room; scroll up by any amount and it is back. It never hides within its own height of the top, and Tab into it brings it back too.

Scroll down and the bar slides away to give the text the room; scroll up by any amount and it is back. It never hides within its own height of the top, and Tab into it brings it back too.

The hide is a move even under reduced motion, and that is a considered exception to the library's rule of collapsing geometry. A bar that faded in place would still cover the content under it and swallow the taps meant for that content; one that also gave up pointer events would have to leave the accessibility tree to be consistent, taking the page's landmark with it. So the bar moves — a state change the page asked for, not an ornament — on a duration that reduced motion shortens rather than removes.

A centred measure

A marketing header wants its content lined up with the page column below it while the surface still runs edge to edge. That is one knob, not a prop:

<AppBar.Root style={{ "--forte-app-bar-max-inline-size": "72rem" }}>

Extra width becomes padding on both sides, and the edge padding is still respected below the measure.

Accessibility

The landmark. AppBar.Root renders a <header>, which is the page's banner landmark when it is not inside <main>, an <article>, a <section> or a <nav>. A bar that heads a panel or a dialog body rather than the page should not be a banner — pass render={<div />}, or give it aria-label if it should stay a landmark with a name of its own.

Headings. AppBar.Title renders a <div>, not a heading — the same bar heads an h1 screen and an h2 panel, and a hardcoded level would put a wrong entry in the outline on one of them. Nest the heading when the bar genuinely names the document:

<AppBar.Title>
  <h1>Inbox</h1>
</AppBar.Title>

A heading placed there takes the bar's typography rather than the UA's, so the semantic version looks identical to the plain one.

Names. Every icon-only control in the bar needs its own aria-label — an icon is not an accessible name — and the Button's iconOnly holds the 24×24 minimum hit target from SC 2.5.8 for you.

Focus. A bar hidden by hideOnScroll stays in the tree and focusable; it slides back the moment a control inside it receives focus, so keyboard users reach everything a pointer user can.

Under forced colours the fill, the blur and the shadow are stripped, and the hairline is the whole bar: every variant paints it in the system ink, plain included. Text and icons take CanvasText, and frosted is opaque there and under prefers-contrast: more alike.

Theming

Everything below is declared on AppBar.Root itself, which is also why an ancestor is the wrong place to set one — the element's own declaration beats an inherited value. Override them on the root through its className or a style object, or re-point the global tokens in the Default column to move every bar at once.

Theming tokens for AppBar
PropertyControlsDefault
--forte-app-bar-pxInline padding at the bar's edges. size re-points it.var(--forte-control-px-md)
--forte-app-bar-pyBlock padding above and below the row. size re-points it.var(--forte-space-3)
--forte-app-bar-min-block-sizeMinimum height of the bar, padding and hairline included. size re-points it to the matching global --forte-app-bar-h-* token — the control height plus the padding — so the bar fits its own buttons and the layout around it can read the same number from the token.var(--forte-app-bar-h-md)
--forte-app-bar-gapGap between the leading slot, the title and the trailing slot.var(--forte-space-3)
--forte-app-bar-item-gapGap between the items inside a slot. Tighter than the slot gap, so two icon buttons read as a pair.var(--forte-space-1)
--forte-app-bar-max-inline-sizeWidth the row is centred within. 100% is edge to edge; set a measure — 72rem — for a marketing header whose content should line up with the page column below it. Extra width becomes padding.100%
--forte-app-bar-title-font-sizeSize of the title. size re-points it.var(--forte-font-size-3)
--forte-app-bar-title-font-weightWeight of the title.var(--forte-font-weight-semibold)
--forte-app-bar-inset-block-startDistance from the top of the scroll container while sticky or fixed — the height of another bar above this one, typically. The scroll tracking reads it, so data-scrolled still lands on time.0px
--forte-app-bar-z-indexStacking level while sticky or fixed. Popups are portaled to <body> and never compete with it; this is for the page's own positioned content.10
--forte-app-bar-border-widthWidth of the hairline under the bar. Reserved even when the variant paints it transparent, so elevateOnScroll gaining an edge moves nothing, and so forced colours have a border to recolour.1px
--forte-app-bar-scrolled-shadowShadow the bar gains under elevateOnScroll once scrolled.var(--forte-shadow-2)
--forte-app-bar-glass-alphaOpacity of the frosted fill. Defaults to the global glass alpha, which prefers-reduced-transparency already raises to 1.var(--forte-glass-alpha)
--forte-app-bar-blurBlur radius behind a frosted bar.12px
--forte-app-bar-durationDuration of the surface changes — fill, edge, shadow.var(--forte-duration-normal)
--forte-app-bar-move-durationDuration of the hide.var(--forte-duration-normal)
--forte-app-bar-easeEasing for everything the bar animates.var(--forte-ease-standard)
--forte-app-bar-bgFill behind the bar. variant and tone set it.var(--forte-app-bar-tone-solid)
--forte-app-bar-fgText and icon colour, inherited by everything inside. variant and tone set it.var(--forte-app-bar-tone-on-solid)
--forte-app-bar-fg-mutedMuted text colour, handed to descendants as --forte-color-foreground-muted on the coloured tones.var(--forte-app-bar-tone-on-solid-muted)
--forte-app-bar-hoverHover fill handed to the ghost controls inside a coloured bar.var(--forte-app-bar-tone-solid-hover)
--forte-app-bar-activeActive fill handed to the ghost controls inside a coloured bar.var(--forte-app-bar-tone-solid-active)
--forte-app-bar-border-colorColour of the hairline under the bar. variant and tone set it.var(--forte-app-bar-tone-solid-border)
--forte-app-bar-separatorRule colour handed to Separators inside a coloured bar.var(--forte-app-bar-tone-solid-separator)
--forte-app-bar-shadowDrop shadow. none until elevateOnScroll has something to lift.none

The surface slots — --forte-app-bar-bg, -fg, -border-color, -shadow — are the whole variant system: variant and tone fill them in, and a one-off look is those properties on one element. The three hand-off slots — -fg-muted, -hover, -active — plus -separator are what a coloured bar passes down to its subtree as the global foreground tokens; on a neutral bar they are declared but not handed down, because the neutral slots read those very tokens and a custom property that references itself is a cycle.

State is on data-variant, data-tone, data-size, data-position, data-scrolled and data-hidden, reachable from plain CSS or a Tailwind arbitrary variant without a wrapper element. Part markers are data-forte="app-bar" on the root and app-bar-leading, app-bar-title, app-bar-trailing on the parts; a bar that tracks scroll also renders app-bar-sentinel, the 1px sibling the observer watches.

API reference

Every part is a plain element and takes that element's whole prop surface, ref included. Only AppBar.Root has a render prop, for swapping the <header>.

AppBar.Root

Props for AppBarRoot
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
elevateOnScrollbooleanfalseStart with no surface at all and gain the variant's fill, hairline and a shadow once the page has scrolled under the bar. The Material "on scroll" behaviour: the bar reads as part of the page at the top and as a surface over it everywhere else. `plain` has no surface to reveal, so pair this with another variant.
hideOnScrollbooleanfalseSlide the bar out of view while the page scrolls down and back in as soon as it scrolls up. The bar never hides within its own height of the top, and a control inside it that holds focus keeps it on screen.
positionAppBarPositionstaticHow the bar is attached to the page. `static` scrolls away with the content; `sticky` pins to the top of its scroll container once reached and needs no layout compensation; `fixed` leaves the flow entirely, so the content under it must add its own top padding. Both scroll-aware features — `elevateOnScroll` and `hideOnScroll` — need the bar to still be on screen when the page moves, so both are ignored while `static`.
renderRenderProp<Record<string, unknown>>Replaces the rendered `<header>` with another element or component — `render={<div />}` for a bar that is not the page's banner, such as one heading a panel or a dialog body.
scrollThresholdnumber0How far the scroll container has to move, in pixels, before the bar counts as scrolled. `0` is the first pixel past the bar's resting position; `80` holds the bar in its at-rest state until the page has moved 80px under it. Negative values are clamped to `0` — the bar cannot be scrolled before it is reached. This moves `data-scrolled`, not the elevation: everything keyed on that attribute shifts with it — `elevateOnScroll`, and any rule of your own reading `[data-scrolled]` for a border, a background or a shorter title. `hideOnScroll` is the exception and is unaffected, because `data-hidden` follows scroll *direction* rather than distance. Ignored while `static`, along with the rest of the scroll tracking.
sizeAppBarSizemdHeight of the bar, and the size of its title. Each step is the matching control height plus the bar's padding, so a `size="sm"` bar fits `size="sm"` buttons exactly. The controls inside keep their own `size` prop — this sets the bar, not its contents. The height of each step is a global token, `--forte-app-bar-h-sm` / `-md` / `-lg`, so the layout around the bar — a sticky sidebar's offset, a page's `scroll-padding-top` — can read it without knowing how the bar is built.
toneAppBarToneneutralWhich colour set the bar draws from. `neutral` is the grey scale; `primary` and `secondary` fill the bar with the accent and re-point the foreground tokens inside it, so the ghost buttons, links and separators you compose into it recolour themselves for contrast.
variantAppBarVariantpanelHow much chrome the bar carries. `plain` is a bare row with no surface; `panel` fills with the panel colour; `outline` sits on the page background with a hairline underneath; `frosted` is `outline` made translucent, with the content behind it blurred. `frosted` is an enhancement in two directions: where `backdrop-filter` is unsupported the bar is simply translucent, and for a reader who has asked the OS to reduce transparency it is opaque.

AppBar.Leading

Props for AppBarLeading
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

AppBar.Title

Props for AppBarTitle
PropTypeDefaultDescription
alignAppBarTitleAlignstartWhere the title sits in the space between the leading and trailing slots. `center` is the phone convention — the title between a back button and an action — and it is centred in *that* space, so a bar with controls on only one side puts it off the bar's true middle.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

AppBar.Trailing

Props for AppBarTrailing
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.