Catalog · Navigation
App header
The quiet bar at the top of an application's main area: the menu button, the product name on a phone, the search and the account.
Fits best: app — the top of an application's main area, beside the sidebar
Example
The header of an application, with the menu button, the product name and the account list.
The bar at a window of full width: the sidebar is a column beside it, so the product name stays there. At 768 px and below the sidebar is a drawer, the menu button opens it and the product name shows beside it.
The example is not sticky: in a page, .o-sticky keeps the bar at the top. The account list opens under the button: press it.
Known issues
The header's own issues are those of the pieces of core it is made of.
- The account list without anchor positioning. In browsers without CSS anchor positioning, the list opens as a sheet at the bottom of the window instead of under the button. Escape, or a click outside it, closes it.
- Sticky bar and enlarged text. The scroll padding of
.o-stickyis a minimum: at 300% text, with the bar wrapped to several rows, Safari can leave a field reached with Tab under it. Setscroll-padding-block-startonhtmlto your header's real height, or leave out.o-sticky.
Code
Copy the markup
import "@luz-ds/catalog/components/app-bar.css";
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/components/app-bar.min.css" integrity="sha384-coazETaShJ8FGQ4mRfMwvCnFxdigv1KQ6ShOIpp7Uu6aaRTzQcuPJJdYCKDLwiLM" crossorigin="anonymous">
The bar, in .o-main beside the sidebar, with catalog's search before the account:
<div class="o-shell">
<nav class="luz-sidebar" id="app-nav" aria-label="Main" popover>…</nav>
<div class="o-main">
<header class="luz-app-bar o-sticky">
<div class="o-row">
<button class="luz-nav-toggle" type="button" popovertarget="app-nav" aria-label="Menu">
<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h16"/></g></svg>
</button>
<p class="luz-app-bar__product">Bodega</p>
<div class="o-row luz-app-bar__actions">
<luz-search>…</luz-search>
<div class="luz-dropdown">
<button class="luz-btn luz-btn--ghost luz-app-bar__account" type="button" popovertarget="account-menu"><span class="luz-avatar luz-avatar--sm">AM</span> <span class="luz-app-bar__account-name">Ana Martín</span></button>
<div class="luz-dropdown__menu" id="account-menu" popover>
<ul class="luz-dropdown__list">
<li><a class="luz-dropdown__item" href="/account/">Account settings</a></li>
<li><a class="luz-dropdown__item" href="/logout/">Sign out</a></li>
</ul>
</div>
</div>
</div>
</div>
</header>
<main class="luz-dashboard" id="main-content" tabindex="-1">…</main>
</div>
</div>
Variants and options
No modifier. The header is a class of app on a native <header>, laid out by core's objects in the markup.
| Class | Effect |
|---|---|
.luz-app-bar | On <header>, a child of .o-main: the page surface and a rule underneath. It is one row of --luz-bar-h; its .o-row has the gutters of the content |
.luz-app-bar | The product's name in medium text, semibold. It shows only at 768 px and below, where the sidebar is a drawer and its brand is out of sight |
.luz-app-bar | On a .o-row: the global tools at the inline end of the bar. The catalog's search, then the account |
.luz-app-bar | On the .luz-btn.luz-btn that opens the account list: at least 44 by 44 px, with its initials in a .luz-avatar and then the name |
.luz-app-bar | The account's name. At 640 px and below it is clipped to nothing, and stays in the button's name |
.o-sticky (core) | Keeps the bar at the top while the page scrolls, in a window at least 30 rem tall; see the page and its bars |
.luz-dropdown (core) | The account list, a disclosure of links: see the dropdown |
How it works
CSS from components/app-bar.css of @luz-ds/catalog on core's shell, sticky bar, drawer and dropdown. No JavaScript.
- App adds the skin only: the page surface, the rule underneath and the gaps of the row (8 px between rows, 16 px between items). Core gives the rest:
.o-stickythe stickiness and the scroll padding,.o-rowthe row, the nav toggle and the sidebar the drawer, and the dropdown the account list. - A one-row bar is core's
--luz-bar-htall: 44 px controls, 4 px above and below, and the 1 px rule. With enlarged text the row wraps and the bar grows. - The page is what scrolls, not
.o-main, so the sticky bar stays at the window's top. If you give.o-maina height of its own and make it scroll, what it focuses stays clear of the bar through its scroll padding. - The account list opens under its button and ends where the button ends, so it never leaves the window at the inline end of the bar.
- Without the catalog there is no search: leave
<luz-search>out. Nothing else changes. - The view's
h1and its main action are not here: they are the first row of the dashboard. The bar holds only what is the same in every view.
Without JavaScript
Nothing changes.
The drawer and the account list are popovers opened by the button's popovertarget, with no script, and catalog's search shows its link to the A–Z index. In a browser without the popover API the sidebar stays in the page above the content, the menu button is not shown, and the account list stays under its button.
Keyboard and ARIA
- Order: the order of the source and of the screen. Skip link, the sidebar (a column from 769 px), the search, the account, then the view. At 768 px and below the closed drawer is out of the order: skip link, menu button, search, account, then the view.
- Keys: Enter or Space on the menu button opens the drawer, and on the account button opens the list; Tab reaches each link in order; Escape closes it and returns focus to the button.
- Names: the menu button is named "Menu" (
aria-label, translated like the rest of the page). The account button's name is its initials, then the name: "AM Ana Martín", so a person who says "click AM" finds it at any width. The browser exposes the buttons' expanded state. - Not a menu: the account list is a list of links in a popover. It has no
role="menu", noaria-haspopupand no arrow keys. - Landmarks: a
<header>in.o-main, which is a<div>, is the page's banner, beside the<main>, never inside it. The sidebar is the one navigation, named "Main". Put the skip link first: it skips the sidebar and the bar.
Contrast and focus are in the Design tab.
Design
When to use
Use the header at the top of the main area of every view of an application: the person always finds the search and their account in the same place.
- One quiet row of global tools: the menu button, the search and the account.
- The product's name on a phone, where the sidebar's brand is behind the drawer.
When not to use
Do not use it for the title of the view or for its actions: they go in the first row of the dashboard.
- One header per page, in
.o-main: it is the page's banner. - Do not put a primary button in the bar: the view's main action would be lost among the chrome.
- For a bar with the site's own sections, use the navbar in the site header.
Do and don't
Writing the content
The product is a name; the account is a person; the list holds what they can do about themselves.
- Product: the name as it appears in the sidebar's brand. Never shorten it: it wraps.
- Account: the person's name in full, and the initials of that name in the avatar. A photo has an empty
alt, so the name is the button's name. - List: two to four links, each a verb or a noun the person knows: "Account settings", "Sign out". Put "Sign out" last.
- Menu button: "Menu", translated like the rest of the page.
Contrast and focus
Text and controls sit on the page surface; the initials sit on the brand's subtle fill.
| Part | Colour on background | Light | Dark | High contrast | Needs |
|---|---|---|---|---|---|
Product name | --luz-text-primary on --luz-surface-page | 15.8:1 | 15.1:1 | 21.0:1 | 4.5:1 |
Account name | --luz-brand-primary on --luz-surface-page | 5.6:1 | 7.7:1 | 14.1:1 | 4.5:1 |
Initials | --luz-brand-primary on --luz-brand-subtle over --luz-surface-page | 5.1:1 | 6.5:1 | 13.6:1 | 4.5:1 |
Focus ring | --luz-focus-ring on --luz-surface-page | 5.6:1 | 7.7:1 | 21.0:1 | 3:1 |
Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons.
- Focus: every control in the bar keeps core's ring, 2 px of
--luz-focus-ring. The bar is sticky, and.o-stickykeeps what Tab reaches out from under it. - Edges: the rule under the bar (
--luz-divider) is decorative and has no minimum; forced colours keep it. - Target: the account button is at least 44 by 44 px at every width, with its name clipped.
Tokens it uses
The 10 tokens that the .luz-app-bar rules of catalog’s components/app-bar.css read, by family. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.
- Spacing and sizing
--luz-control-lg,--luz-space-2xs,--luz-space-md,--luz-space-xl,--luz-space-xs- Typography
--luz-text-md- Layout and breakpoints
--luz-bar-h
Keyboard and ARIA are in the Code tab.