Skip to content

Top bar / app bar

In review
Live · web

Primary app header: drive it live (toggle search, push the inbox count, condense the bar)

NockerlDashboard

Inbox shows 3 unread · search is collapsed · bar is full. Tab through every action; the cyan boundary line closes the bar. The island is live.

Variants

Chat
Session settings

Condensed: the same bar after scroll (height collapses; freezes under reduced motion)

NockerlDashboard
3 selected
// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform. Spec + live demo,
// not yet exported from @dizyx/nockerl-react; promotion is tracked.)
import { NockerlIconButton, NockerlAvatar, NockerlIcon } from '@dizyx/nockerl-react';
// Glyphs are inline SVG nodes: `icon` takes a ReactNode, not an icon name. The
// NockerlIcon registry carries only a few named paths, so pass a bespoke path for the rest.
const SearchIcon = <NockerlIcon><circle cx="11" cy="11" r="7" /><path d="m21 21-4.3-4.3" /></NockerlIcon>;
const BackIcon = <NockerlIcon><path d="M19 12H5" /><path d="m12 19-7-7 7-7" /></NockerlIcon>;
export function AppHeader() {
const [searchOpen, setSearchOpen] = useState(false);
return (
<TopBar
brand={{ mark: 'nockerl', product: 'Nockerl' }} // mark + optional product label
title="Chat" // page title (centered on Android)
align="center" // 'lead' | 'center'
actions={
<>
<NockerlIconButton icon={SearchIcon} label="Search" pressed={searchOpen} onClick={() => setSearchOpen((v) => !v)} />
<ClusterStatus online={3} total={4} onClick={openCluster} />
<InboxButton count={3} onClick={openInbox} /> {/* count → "99+" → unseen dot */}
<NockerlAvatar src={user.photo} name={user.name} presence="streaming" onClick={openSettings} />
</>
}
/>
);
}
// A sub-screen header: a leading back button + title instead of the brand.
<TopBar leading={<NockerlIconButton icon={BackIcon} label="Back" onClick={goBack} />} title="Session settings" />
PropTypeDefaultDescription
brand{ mark: BrandMark; product?: string }Leading identity: the three-peaks mark and an optional product label. Omit when leading is set (a sub-screen).
leadingReactNodeReplaces the brand on a sub-screen, typically a back IconButton. Mutually exclusive with brand.
titlestringPage title. Bound to the title.medium type role.
align'lead' | 'center''lead'Title placement. center is the Android idiom, absolutely centered to the bar so the action cluster never shifts it.
breadcrumbCrumb[]Wayfinding trail ({ label, href }[]) shown in place of the title; the last crumb is aria-current="page".
actionsReactNodeTrailing account cluster, right-aligned and evenly gapped: search · ClusterStatus · InboxButton · Avatar.
condensedbooleanfalseScrolled state: the bar shrinks its height. Animates (height); freezes under prefers-reduced-motion.
selection{ count: number; actions: ReactNode }Contextual mode. Replaces the chrome with a cyan-tinted "N selected" bar + bulk actions and a clear button.
InboxButton.countnumber0Inbox badge count (agent-orange keycap); clamps to 99+, falls back to an unseen dot at 0.
Cross-platform drift (tracked for reconciliation)
  • Top-bar vs. app-shell vs. toolbar: the top bar is the global header row (brand
    • title / breadcrumb + account cluster). app-shell is the whole layout that hosts it (header + nav region + content over the facet field); toolbar is a contextual action strip (editor / formatting / selection actions). They share the chrome look but solve different jobs. Don’t use one for another.
  • Title placement: Android centers the mark to the screen (a Box) with the account cluster around it; web defaults to a lead-aligned title and offers align="center". The macOS idiom anchors identity in the command pill instead of a header row.
  • Boundary: Android draws the 1.5dp cyan line + a drag handle that collapses the header chrome; web mirrors the 1.5px line (no collapse handle yet); Voice carries the 1.5pt cyan boundary as the command-pill outline, not a full-width line.
  • No single web component yet: @dizyx/nockerl-react’s TopBar API above is the intended shape, not yet published.