Skip to content

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/react

For 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.

Reference

Propssticky, condense, scrollContainer… (26)

Props

AppHeader props
PropType
stickyboolean

Stays at the top of its scroll container.

default true

true
condenseboolean

Condenses once content scrolls under it.

default true

true
scrollContainerRefObject<HTMLElement | null>

The element that scrolls under the header. Default: the page.

–
Other props pass through: extends ComponentProps<'header'>.

AppHeaderSearch

AppHeaderSearch props
PropType
placeholderstring

The field-like label.

default "Search…"

"Search…"
shortcutstring | false

The key shown with ⌘ / Ctrl, matching the palette's shortcut.

default 'k'

'k'
variantButtonVariant
'default''danger''ghost''outline''link''secondary'

default | secondary | outline | ghost | danger | link.

–
loadingboolean

Swaps 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 false) on any button that can load, so its live region exists before the first announcement. Without it, a click handler that returns a promise puts the button in this state until the promise settles; an explicit loading (even false) wins. For a success or error label after the work, use AsyncButton.

–
loadingLabelstring

Announced when loading starts.

default "Loading"

"Loading"
size'default' | 'sm' | 'lg' | 'xs' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg'–
soundboolean

With sound on (YoujProvider’s sound), the button plays tap as it's pressed. false keeps this one silent.

default true

true

It takes the rest of Button's props, such as onClick.

AppHeaderMenu

AppHeaderMenu props
PropType
titlestring

Names the drawer and its navigation.

default "Menu"

"Menu"
openboolean–
defaultOpenboolean

default false

false
onOpenChange(open: boolean) => void–
headerReactNode

Shown above the links, e.g. the brand or a workspace switcher.

–
footerReactNode

Pinned to the bottom of the drawer, e.g. sign-in buttons.

–
classNamestring–
childrenRequiredReactNode

The drawer's navigation: AppHeaderMenuLink rows, groups, anything.

–
AppHeaderMenuLink props
PropType
activeboolean

The 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

Motion tokens
MomentTokens
Condenseheight 8px shorter, surface, blur, hairline and shadow, together
duration.baseease.standard

220ms · cubic-bezier(0.2, 0, 0, 1)

220mscubic-bezier(0.2, 0, 0, 1)
Relaxthe same properties back, from wherever they are
duration.baseease.standard

220ms · 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.standard

140ms · 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.standard

220ms · cubic-bezier(0.2, 0, 0, 1)

220mscubic-bezier(0.2, 0, 0, 1)
Sheet infrom the inline end (yj-sheet-*)
duration.slowease.enter

340ms · cubic-bezier(0.16, 1, 0.3, 1)

340mscubic-bezier(0.16, 1, 0.3, 1)
Sheet out
duration.baseease.exit

220ms · cubic-bezier(0.4, 0, 1, 1)

220mscubic-bezier(0.4, 0, 1, 1)
Menu link hoverbackground and text colors
duration.fastease.standard

140ms · 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 header element, so it's the page's banner landmark when it sits outside main. Give the navigation inside it its own aria-label ("Main").
  • The search button is named "Search" at every width, and announces its shortcut with aria-keyshortcuts (Meta+K or Control+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 nav named by title, and the current one carries aria-current="page" when you set active.
  • Every control in the bar meets the control-height target, which grows by 8px on touch screens.