App Header
Bring your app’s navigation, search and account controls into a header that condenses on scroll.
import { AppHeader, AppHeaderActions, AppHeaderBrand, AppHeaderMenu, AppHeaderMenuLink, AppHeaderNav, AppHeaderSearch } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use it as the one bar across the top of an app or a marketing site: the logo, the primary navigation (plain links or a Mega Menu), a search button for the Command Palette, a few actions and the Account Menu. An app whose navigation lives down the side wants a Sidebar instead, often with a slimmer header beside it. On phones, where the primary destinations belong under the thumb, pair it with a Bottom Tab Bar.
import {
AccountMenu,
AppHeader,
AppHeaderActions,
AppHeaderBrand,
AppHeaderMenu,
AppHeaderMenuLink,
AppHeaderNav,
AppHeaderSearch,
} from '@youj/react'
import { CommandGroup, CommandItem, CommandPalette } from '@youj/react/command'
import { useState } from 'react'
export function Header() {
const [palette, setPalette] = useState(false)
return (
<AppHeader>
<AppHeaderBrand href="/">
<Logo />
Northwind
</AppHeaderBrand>
<AppHeaderNav>
<MainNavigation />
</AppHeaderNav>
<AppHeaderActions>
<AppHeaderSearch onClick={() => setPalette(true)} />
<AccountMenu user={user} onSignOut={signOut} />
<AppHeaderMenu header="Northwind">
<AppHeaderMenuLink href="/deployments" active>
Deployments
</AppHeaderMenuLink>
<AppHeaderMenuLink href="/analytics">Analytics</AppHeaderMenuLink>
</AppHeaderMenu>
</AppHeaderActions>
<CommandPalette open={palette} onOpenChange={setPalette}>
<CommandGroup heading="Go to">
<CommandItem onSelect={() => setPalette(false)}>Deployments</CommandItem>
</CommandGroup>
</CommandPalette>
</AppHeader>
)
}The parts are slots in one row. AppHeaderBrand is a link (pass render for your router's link), AppHeaderNav holds the wide-screen navigation and hides below md, and AppHeaderActions pushes everything after it to the inline end. AppHeaderMenu is the phone navigation: a hamburger that only shows below md, opening a Sheet. Put the same destinations in both.
Description
A header with a brand, navigation, a search button that opens a command palette, an account menu and a phone menu, over a scrolling page.
The header is sticky and starts flat, on the page's own background. Once content scrolls under it, it condenses: 8px shorter, a translucent blurred surface, a hairline and a soft shadow, so what passes beneath reads as beneath. It condenses past 24px and relaxes under 8px. That gap keeps it from flickering when someone scrolls back and forth around the threshold, and a reversal mid-transition carries on from where it is.
The bar's height is the control height plus padding, so a compact or comfortable theme scales it with its controls. It tracks the page by default. Pass scrollContainer when the content scrolls inside an element, as the preview does, and condense={false} to keep it flat.
AppHeaderSearch looks like a field, but it's a button: the palette is the field. On wide screens it shows its placeholder and the shortcut, ⌘K on Apple platforms and Ctrl K elsewhere. Below md it becomes an icon button. It's named "Search" at every size and announces its shortcut through aria-keyshortcuts. Set shortcut to the same key as the palette's own shortcut (the preview uses j, because this site's search already owns ⌘K), or false to hide it.
Below md, the hamburger's bars morph into a cross as the sheet slides in from the inline end, and the sheet's own cross lands exactly where the hamburger was. header sits at the top of the sheet (the brand, or a Workspace Switcher) and footer is pinned to the bottom. Every AppHeaderMenuLink closes the sheet as it's followed, and the sheet closes itself if the window widens past md. Open this page on a phone, or narrow the window, to try it.
A header that isn't sticky, with a Mega Menu and sign-up actions for visitors who are signed out.
sticky={false} lets the bar scroll away with the page. It keeps its surface from the start, since nothing will pass under it. Here it carries a Mega Menu and the sign-up actions, which hide below md and reappear in the phone menu's footer.
Code
app-header-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propssticky, condense, scrollContainer… (26)
Props
| Prop | Type | |
|---|---|---|
sticky | booleanStays at the top of its scroll container. default true | true |
condense | booleanCondenses once content scrolls under it. default true | true |
scrollContainer | RefObject<HTMLElement | null>The element that scrolls under the header. Default: the page. | – |
Other props pass through: extends ComponentProps<'header'>. | ||
AppHeaderSearch
| Prop | Type | |
|---|---|---|
placeholder | stringThe field-like label. default "Search…" | "Search…" |
shortcut | string | falseThe key shown with ⌘ / Ctrl, matching the palette's default 'k' | 'k' |
variant | ButtonVariant'default''danger''ghost''outline''link''secondary'
| – |
loading | booleanSwaps the label for a spinner without changing the button's width, sets
aria-busy, and blocks presses while keeping keyboard focus on the button.
Pass it (even as | – |
loadingLabel | stringAnnounced when loading starts. default "Loading" | "Loading" |
size | 'default' | 'sm' | 'lg' | 'xs' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg' | – |
sound | booleanWith sound on ( default true | true |
It takes the rest of Button's props, such as onClick.
AppHeaderMenu
| Prop | Type | |
|---|---|---|
title | stringNames the drawer and its navigation. default "Menu" | "Menu" |
open | boolean | – |
defaultOpen | booleandefault false | false |
onOpenChange | (open: boolean) => void | – |
header | ReactNodeShown above the links, e.g. the brand or a workspace switcher. | – |
footer | ReactNodePinned to the bottom of the drawer, e.g. sign-in buttons. | – |
className | string | – |
childrenRequired | ReactNodeThe drawer's navigation: AppHeaderMenuLink rows, groups, anything. | – |
AppHeaderMenuLink
| Prop | Type | |
|---|---|---|
active | booleanThe current page. default false | false |
Other props pass through: extends useRender.ComponentProps<'a'>. | ||
AppHeaderBrand takes an anchor's props and render. AppHeaderNav and AppHeaderActions are divs.
MotionCondense, relax, thresholds… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Condenseheight 8px shorter, surface, blur, hairline and shadow, together | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Relaxthe same properties back, from wherever they are | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Thresholdscondenses past 24px, relaxes under 8px | no token hysteresis | hysteresis | |
Hamburger to crossthe outer bars slide to the middle as the middle one fades (MenuIcon) | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Cross turns45° each way, once the bars have met | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
Sheet infrom the inline end (yj-sheet-*) | duration.slowease.enter340ms · cubic-bezier(0.16, 1, 0.3, 1) | 340mscubic-bezier(0.16, 1, 0.3, 1) | |
| Sheet out | duration.baseease.exit220ms · cubic-bezier(0.4, 0, 1, 1) | 220mscubic-bezier(0.4, 0, 1, 1) | |
| Menu link hoverbackground and text colors | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
The bar condenses and relaxes at once. The hamburger swaps to a cross and the sheet appears without sliding.
Reduced motion
The bar takes its condensed or relaxed state at once, with no height, blur or shadow transition. On phones the hamburger turns straight into a cross and the sheet appears without sliding in. Focus handling and the thresholds are unchanged.
AccessibilityKeyboard, ARIA, focus
Accessibility
- It's a
headerelement, so it's the page's banner landmark when it sits outsidemain. Give the navigation inside it its ownaria-label("Main"). - The search button is named "Search" at every width, and announces its shortcut with
aria-keyshortcuts(Meta+KorControl+K). - The hamburger is named "Open menu" (from
title), and the sheet is a modal dialog named by its header. Focus is trapped inside while it's open, Escape closes it, and focus returns to the hamburger. The sheet's cross is named "Close menu". - The sheet's links sit in a
navnamed bytitle, and the current one carriesaria-current="page"when you setactive. - Every control in the bar meets the control-height target, which grows by 8px on touch screens.