Top Header Mobile
Mobile top app bar with leading/trailing slots, centred or start-aligned title, subtitle, six variants, five sizes, tokened bottom rounding, three layout modes, scroll elevate/collapse behavior, search and sub-row decks, Surface tone, and full RTL support.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { TopHeaderMobile } from "@tessinaui/ui";
import { IconButton } from "@tessinaui/ui";<TopHeaderMobile
bordered
title="Inbox"
subtitle="3 unread"
leadingAction={<IconButton variant="ghost" size="sm" icon={<ArrowLeft />} aria-label="Back" />}
trailingActions={<IconButton variant="ghost" size="sm" icon={<Search />} aria-label="Search" />}
/>Examples
Default
Back, dead-centred title, one trailing action — the dominant single-row shape, composed from library IconButtons. The second bar names the back destination (a ghost Button in the same slot).
Variants
Six paints — filled, elevated, transparent, blur, primary, on-color.
The coloured variants publish the Surface tone, so nested library controls
re-ink themselves.
Sizes
Bar min-heights 48 / 56 / 64 / 80 / 96px. xs, sm and md land exactly on
Carbon's 48px shell, Atlassian's 56px global nav and M3's 64px small bar.
Layout modes
M3 lineage: small inline, medium and large drop the title below the
action row. Content-hugging — a subtitle simply grows the bar, and the large
row wraps up to two lines instead of truncating.
Scroll behavior
scrollBehavior="elevate" paints a hairline + shadow once content scrolls
under the sticky bar; "collapse" additionally folds a medium/large bar to
the inline title (M3 compress / iOS large-title fold). An
IntersectionObserver sentinel drives both — no scroll listeners.
Subtitle
Presence, status, count or context under the title — in centred, start-aligned and large layouts. The chat header composes an Avatar into the title cluster.
Title alignment
center holds the title dead-centre regardless of the side clusters; start
left-aligns after the leading slot. Both slots are optional.
Title menu
The title as a dropdown trigger — the feed/workspace switcher, iOS 16's
titleMenuProvider pattern. Pure composition with DropdownMenu.
Search
The search slot renders its own deck under the action row inside a search
landmark. For a field that replaces the title, pass it as the title.
Sub row
The structured second deck — tabs, segmented control, or a scrolling chip rail — padded to the bar's own gutter.
Transparent over media
The bar paints nothing; the actions carry their own fill (chip-style
IconButtons), tone="on-color" re-inks any text, and the H1 stays in the
content.
Select mode
Cancel + live count + one prominent Done — pure composition, no mode prop.
Commerce
Brand mark in the title slot, a filled CTA, wishlist and a cart count on IconButton's own badge.
Skip link
WCAG 2.4.1: TopHeaderMobileSkipLink hides until keyboard focus, then pins
over the bar's leading corner. Click into the demo and press Tab.
Loading
The skeleton mirrors the bar's box — same cva, same size maps, icon circles on IconButton's size tokens, text bars sized in characters.
API Reference
TopHeaderMobile
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "filled" | "elevated" | "transparent" | "blur" | "primary" | "on-color" | "filled" | Visual style. primary/on-color publish the on-color Surface tone |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Bar min-height 48/56/64/80/96px |
rounded | "none" | "sm" | "md" | "lg" | "full" | "none" | Bottom-corner rounding; full = the tokened panel scale |
title | ReactNode | — | Title text or custom element |
titleAs | "h1" | "h2" | "h3" | "span" | "h1" | Heading element for a string title. Use "span" when the page H1 lives in the content |
titleAlign | "start" | "center" | "center" | Inline-title alignment |
subtitle | ReactNode | — | Second line under the title (status, presence, context) |
leadingAction | ReactNode | — | Leading slot (back/menu control) |
trailingActions | ReactNode | — | Trailing slot (icon buttons, text actions) |
layout | "small" | "medium" | "large" | "small" | Inline title, or title below the row at two scales |
collapsed | boolean | — | Fold medium/large to inline. Leave unset with scrollBehavior="collapse" for self-folding; set it and the prop wins |
sticky | boolean | false | sticky top-0 + safe-area top inset |
bordered | boolean | false | Hairline bottom border |
scrollBehavior | "none" | "elevate" | "collapse" | "none" | Scroll response — elevate chrome, or elevate + collapse. Pair with sticky |
search | ReactNode | — | Search deck row under the bar, wrapped in a search landmark |
subRow | ReactNode | — | Structured second deck (tabs, segmented, chips) |
tone | "default" | "on-color" | auto | Surface tone for nested controls; auto-detected from variant, override for transparent-over-media |
skeleton | boolean | false | Swap the bar for its skeleton |
render | useRender.RenderProp | — | Replace the rendered <header> element |
dir | "ltr" | "rtl" | — | Text and layout direction |
asChild was removed — it was declared but never implemented and leaked onto
the DOM. Use render instead.
TopHeaderMobileSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
variant / size / rounded / layout / bordered | same as the bar | — | The skeleton is the same box |
showLeading | boolean | true | Leading circle |
trailingCount | number (0–3) | 1 | Trailing circles |
titleChars | number | 12 | Title bar width, in characters of the real title |
showSubtitle | boolean | false | Subtitle line |
TopHeaderMobileSkipLink
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | "#content" | Jump target. Render it first inside leadingAction |
Keyboard navigation
| Key | Action |
|---|---|
Tab / Shift+Tab | Move focus through the slot controls in DOM order (skip link first when present) |
Enter / Space | Activate the focused control |
The bar itself is not interactive and never traps focus; arrow-key behavior belongs to composed widgets (Tabs, SegmentedControl, DropdownMenu).
Accessibility
- Renders a plain
<header>with no hardcodedrole="banner"— the banner landmark applies only at body level (APG); nested bars stay generic. - A string
titlerenders astitleAs(defaulth1). Set"span"for actions-only bars so the content owns the document outline. - The
searchslot is wrapped in asearchlandmark (element + explicit role for older AT). TopHeaderMobileSkipLinkprovides the WCAG 2.4.1 bypass block.- Slot controls must carry
aria-labels; library IconButton enforces the 44px touch floor on phones. - The skeleton is one
role="status"region announced by sr-only text. - The large-title fold animates opacity only, gated by
motion-safe. - RTL:
dir="rtl"flips the row; the title cluster right-aligns.
Boundaries
- Trailing actions: M3 caps at two icons (one if filled); the observed mobile mode is two, max five. Keep to ≤3 and push overflow into a DropdownMenu — never an in-bar auto-overflow.
- One prominent action. At most one filled/text-emphasized action, on the
trailing side (HIG
.prominent). Everything else stays ghost. - Back vs Close are different verbs. Back returns within a flow; Close dismisses a modal context. A sheet header (top rounding + grabber) belongs to Drawer/ActionSheet, not this bar.
- Full-screen takeover menus belong to Drawer/Command — the bar hosts the trigger only.
- Bottom rounding only. A top bar never curves its top edge; M3 keeps app
bars straight (curvature implies expandability) — our
roundedaxis is the documented divergence, tokened through §2. - Non-sticky bars at the very top of a standalone PWA still sit under the
status bar — add
pt-[env(safe-area-inset-top)]viaclassNamethere, or usesticky.