# Header bar (themes.dev UI block)

Brand, a row of links and one primary button; under 672px of its own width the links fold into a Menu disclosure (Escape closes it). Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

71% of the 2,073 sites in The UI Index have a header (1,473 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/ui/r/header-bar.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/ui/r/astro/header-bar.json`
- HTML: https://www.themes.dev/ui/r/html/header-bar.html (with https://www.themes.dev/ui/r/html/caseroom.css)

Page: https://www.themes.dev/ui/blocks/header-bar/ · Rules for agents: https://www.themes.dev/ui/AGENT.md

## Code

### header-bar.astro

```astro
---
import Button from '@/components/ui/button.astro';
import Icon from '@/components/ui/icon.astro';
import { cn } from '@/lib/utils';
import { headerBar as c, type HeaderBarContent } from './header-bar.classes';

type Props = HeaderBarContent & { class?: string };
const { brand, links = [], action, currentPath, label = 'Main', menuLabel = 'Menu', id = 'header-bar', class: className } = Astro.props;
---

<header data-header-bar class={cn(c.root, className)}>
  <div class={c.bar}>
    <a href={brand.href} class={c.brand}>{brand.label}</a>
    <button type="button" aria-expanded="false" aria-controls={`${id}-menu`} data-header-bar-toggle class={c.toggle}>
      <Icon name="menu" class={c.toggleOpen} />
      <Icon name="x" class={c.toggleClose} />
      {menuLabel}
    </button>
    <div id={`${id}-menu`} data-header-bar-menu class={c.menu}>
      {
        links.length > 0 && (
          <nav aria-label={label} class={c.nav}>
            <ul role="list" class={c.list}>
              {links.map((l) => (
                <li>
                  <a href={l.href} aria-current={l.href === currentPath ? 'page' : undefined} class={c.link}>{l.label}</a>
                </li>
              ))}
            </ul>
          </nav>
        )
      }
      {action && <div class={c.action}><Button href={action.href}>{action.label}</Button></div>}
    </div>
  </div>
</header>

<script>
  import './header-bar.client';
</script>
```

### header-bar.classes.ts

```tsx
/** The header bar: the brand, a row of links and one primary button. Wide enough (672px of the block's own width,
 *  a container query) they sit in one row; narrower, the links and the button fold into a panel under the bar behind
 *  a Menu button (a disclosure: Escape closes it, following a link closes it). Without JavaScript the panel simply
 *  shows under the bar, so every link is reachable. */
export const headerBar = {
  root: 'group/header @container/header relative z-40 border-b bg-background text-foreground',
  bar: 'mx-auto flex max-w-6xl flex-wrap items-center gap-x-4 px-6 py-3 @2xl/header:flex-nowrap',
  brand: '-mx-2 flex h-10 shrink-0 items-center gap-2 rounded-md px-2 text-[15px] font-semibold tracking-[-0.02em] transition-opacity hover:opacity-75 active:opacity-60',
  // Only shown once the script has marked the header (data-js), and only below the fold.
  toggle:
    'group ml-auto hidden h-10 cursor-pointer items-center gap-2 rounded-md border border-input px-3 text-sm font-medium transition-[background-color,transform] hover:bg-accent active:scale-[0.97] active:bg-pressed aria-expanded:bg-accent group-data-[js]/header:@max-2xl/header:inline-flex [&_svg]:size-4',
  toggleOpen: 'group-aria-expanded:hidden',
  toggleClose: 'hidden group-aria-expanded:block',
  menu: 'flex basis-full flex-col gap-4 pb-3 @max-2xl/header:data-collapsed:hidden @2xl/header:basis-auto @2xl/header:flex-1 @2xl/header:flex-row @2xl/header:items-center @2xl/header:gap-6 @2xl/header:pb-0',
  nav: '@2xl/header:ml-2',
  list: 'm-0 flex list-none flex-col p-0 @2xl/header:flex-row @2xl/header:items-center @2xl/header:gap-1',
  link: 'flex min-h-12 items-center rounded-md px-3 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed active:text-foreground aria-[current=page]:text-foreground aria-[current=page]:underline aria-[current=page]:decoration-2 aria-[current=page]:underline-offset-8 @2xl/header:min-h-10 @2xl/header:px-3 @2xl/header:hover:bg-accent @2xl/header:active:bg-pressed',
  action: 'flex flex-col gap-2 @2xl/header:ml-auto @2xl/header:flex-row @2xl/header:items-center',
};

export type HeaderBarLink = { label: string; href: string };
export type HeaderBarContent = {
  brand: { label: string; href: string };
  links?: HeaderBarLink[];
  /** The one primary button. */
  action?: { label: string; href: string };
  /** The link whose href equals this is the current page. */
  currentPath?: string;
  /** The nav's name for screen readers. Default "Main". */
  label?: string;
  /** The Menu button's text below the fold. Default "Menu". */
  menuLabel?: string;
  /** Unique on the page; ids the menu. */
  id?: string;
};
```

### header-bar.client.ts

```tsx
// Header bar behaviour, shared by the Astro, React and HTML versions: the Menu button below the fold. The server HTML
// shows the menu under the bar, so without JavaScript every link is there; enhanceHeaderBar() marks the header with
// data-js (which shows the Menu button) and collapses the menu (data-collapsed). A click (or Enter/Space) opens and
// closes it; Escape closes it and, when focus is inside the header, returns focus to the button; following a link in it closes it.
declare global {
  interface Window {
    __caseroomHeaderBar?: boolean;
  }
}

const ROOT = '[data-header-bar]';

const setMenu = (root: Element, open: boolean, focus = false) => {
  const toggle = root.querySelector<HTMLElement>('[data-header-bar-toggle]');
  toggle?.setAttribute('aria-expanded', String(open));
  root.querySelectorAll('[data-header-bar-menu]').forEach((m) => m.toggleAttribute('data-collapsed', !open));
  if (focus) toggle?.focus();
};

export function enhanceHeaderBar(root: Element) {
  if (root.hasAttribute('data-js')) return;
  root.setAttribute('data-js', '');
  root.querySelectorAll('[data-header-bar-menu]').forEach((m) => m.setAttribute('data-collapsed', ''));
}

if (typeof window !== 'undefined' && !window.__caseroomHeaderBar) {
  window.__caseroomHeaderBar = true;
  document.querySelectorAll(ROOT).forEach(enhanceHeaderBar);
  document.addEventListener('click', (e) => {
    const target = e.target as Element;
    const root = target.closest(ROOT);
    if (!root?.hasAttribute('data-js')) return;
    const toggle = target.closest('[data-header-bar-toggle]');
    if (toggle) return setMenu(root, toggle.getAttribute('aria-expanded') !== 'true');
    if (target.closest('[data-header-bar-menu] a')) setMenu(root, false);
  });
  document.addEventListener('keydown', (e) => {
    if (e.key !== 'Escape') return;
    document.querySelectorAll(`${ROOT}[data-js]`).forEach((root) => {
      if (root.querySelector('[data-header-bar-toggle][aria-expanded="true"]')) setMenu(root, false, root.contains(document.activeElement));
    });
  });
}

export {};
```

### header-bar.tsx

```tsx
import { useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/ui/icon';
import { cn } from '@/lib/utils';
import { enhanceHeaderBar } from './header-bar.client';
import { headerBar as c, type HeaderBarContent } from './header-bar.classes';

export type HeaderBarProps = HeaderBarContent & { className?: string };

/** The site's top bar: brand, links and one primary button; below 672px of its own width the links fold behind a Menu button. */
export function HeaderBar({ brand, links = [], action, currentPath, label = 'Main', menuLabel = 'Menu', id = 'header-bar', className }: HeaderBarProps) {
  const ref = useRef<HTMLElement>(null);
  useEffect(() => {
    if (ref.current) enhanceHeaderBar(ref.current);
  }, []);
  return (
    <header ref={ref} data-header-bar="" className={cn(c.root, className)}>
      <div className={c.bar}>
        <a href={brand.href} className={c.brand}>{brand.label}</a>
        <button type="button" aria-expanded="false" aria-controls={`${id}-menu`} data-header-bar-toggle="" className={c.toggle}>
          <Icon name="menu" className={c.toggleOpen} />
          <Icon name="x" className={c.toggleClose} />
          {menuLabel}
        </button>
        <div id={`${id}-menu`} data-header-bar-menu="" className={c.menu}>
          {links.length > 0 && (
            <nav aria-label={label} className={c.nav}>
              <ul role="list" className={c.list}>
                {links.map((l) => (
                  <li key={l.href}>
                    <a href={l.href} aria-current={l.href === currentPath ? 'page' : undefined} className={c.link}>{l.label}</a>
                  </li>
                ))}
              </ul>
            </nav>
          )}
          {action && <div className={c.action}><Button href={action.href}>{action.label}</Button></div>}
        </div>
      </div>
    </header>
  );
}
```
