# Header Bar

> Brand left, links and a call to action right. Folds into a native details menu when narrow. No JavaScript.

- **Slug:** `header-bar`
- **Kind:** section · **Category:** Site chrome
- **Source:** original to Viewership UI
- **Docs page:** https://viewership.ai/ui/components/header-bar
- **All files in one bundle:** https://viewership.ai/ui/components/header-bar.bundle.txt
- **Structured JSON:** https://viewership.ai/ui/components/header-bar.json

## Install

1. Import the foundation tokens once (download: https://viewership.ai/ui/raw/foundation.css.txt):

```astro
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
---
```

2. Copy this file into `src/components/viewership/header-bar/`:

   - `HeaderBar.astro` (raw: https://viewership.ai/ui/raw/header-bar/HeaderBar.astro.txt)

3. Copy its registry dependencies, each into its own sibling folder under `src/components/viewership/`:

   - `button`: `Button.astro` (docs: https://viewership.ai/ui/components/button.md)

npm packages: none.

## Usage

```astro
---
import HeaderBar from "../components/viewership/header-bar/HeaderBar.astro";
---

<HeaderBar />
```

## Example

The gallery demo, verbatim. Use the import path from Usage instead of `./`.

```astro
---
import HeaderBar from "./HeaderBar.astro";
---

<div class="demo">
  <HeaderBar
    brand="Northwind"
    current="#journal"
    links={[
      { label: "Shop", href: "#shop" },
      { label: "Repairs", href: "#repairs" },
      { label: "Resale", href: "#resale" },
      { label: "Journal", href: "#journal" },
    ]}
    cta={{ label: "Book a repair", href: "#" }}
  />
  <div class="demo__page" aria-hidden="true"></div>
</div>

<style>
  .demo__page {
    height: 260px;
    background: repeating-linear-gradient(to bottom, transparent 0 40px, var(--vs-line-soft) 40px 41px), var(--vs-page);
  }
</style>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `brand` | `string` |  | Brand name, set as text unless the logo slot is used. |
| `brandHref` | `string` | `"/"` | Where the brand links. |
| `links` | `{ label: string; href: string }[]` |  | Main navigation. |
| `cta` | `{ label: string; href: string }` |  | Button at the end of the nav. |
| `current` | `string` |  | Href of the current page (sets aria-current). |
| `sticky` | `boolean` | `false` | Stick to the top while scrolling. |
| `slot: logo` | `slot` |  | Replaces the text brand. |

## Source

### `src/components/viewership/header-bar/HeaderBar.astro`

```astro
---
/**
 * HeaderBar: brand left, links and a call to action right. On narrow widths
 * the links fold into a native <details> menu, so it needs no JavaScript.
 *
 * Mark the current page with `current` (an href) for aria-current.
 *
 * @example
 * <HeaderBar brand="Northwind" links={[{ label: "Shop", href: "/shop" }]} cta={{ label: "Book a repair", href: "/repairs" }} />
 */
import Button from "../button/Button.astro";

interface NavLink {
  label: string;
  href: string;
}
interface Props {
  brand: string;
  brandHref?: string;
  links: NavLink[];
  cta?: NavLink;
  /** Href of the current page. */
  current?: string;
  /** Stick to the top of the viewport while scrolling. */
  sticky?: boolean;
  class?: string;
}

const { brand, brandHref = "/", links, cta, current, sticky = false, class: className } = Astro.props;
---

<header class:list={["vs-header-bar", { "is-sticky": sticky }, className]}>
  <div class="vs-header-bar__inner">
    <a class="vs-header-bar__brand" href={brandHref}>
      <slot name="logo">{brand}</slot>
    </a>

    <nav class="vs-header-bar__nav" aria-label="Main">
      <ul>
        {
          links.map((l) => (
            <li>
              <a href={l.href} aria-current={l.href === current ? "page" : undefined}>
                {l.label}
              </a>
            </li>
          ))
        }
      </ul>
      {cta && <Button href={cta.href} variant="primary" size="sm">{cta.label}</Button>}
    </nav>

    <details class="vs-header-bar__menu">
      <summary>
        <span>Menu</span>
        <span class="vs-header-bar__burger" aria-hidden="true"><i></i><i></i></span>
      </summary>
      <nav class="vs-header-bar__sheet" aria-label="Main">
        <ul>
          {
            links.map((l) => (
              <li>
                <a href={l.href} aria-current={l.href === current ? "page" : undefined}>
                  {l.label}
                </a>
              </li>
            ))
          }
        </ul>
        {cta && <Button href={cta.href} variant="primary">{cta.label}</Button>}
      </nav>
    </details>
  </div>
</header>

<style>
  .vs-header-bar {
    container-type: inline-size;
    position: relative;
    z-index: 10;
    background: var(--vs-page);
    color: var(--vs-ink);
    font-family: var(--vs-font-sans);
    font-stretch: var(--vs-font-stretch);
    border-bottom: var(--vs-border-w) solid var(--vs-line);
  }
  .vs-header-bar.is-sticky {
    position: sticky;
    top: 0;
  }
  .vs-header-bar__inner {
    display: flex;
    min-height: 64px;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    max-width: var(--vs-container);
    margin-inline: auto;
    padding: 0 var(--vs-gutter);
  }
  .vs-header-bar a:not(.vs-button) {
    color: inherit;
    text-decoration: none;
  }
  .vs-header-bar a:focus-visible,
  .vs-header-bar summary:focus-visible {
    outline: none;
    box-shadow: var(--vs-focus-ring);
  }
  .vs-header-bar__brand {
    font-family: var(--vs-font-display, var(--vs-font-sans));
    font-size: 1.25rem;
    font-weight: var(--vs-display-weight);
    letter-spacing: var(--vs-display-tracking);
    text-transform: var(--vs-display-transform);
  }
  .vs-header-bar ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* Inline nav (wide) */
  .vs-header-bar__nav {
    display: none;
    align-items: center;
    gap: 28px;
  }
  .vs-header-bar__nav ul {
    display: flex;
    gap: 24px;
    font-size: 0.9375rem;
  }
  .vs-header-bar__nav a:not(.vs-button) {
    color: var(--vs-ink-2);
    transition: color var(--vs-duration-fast) ease-out;
  }
  .vs-header-bar__nav a:not(.vs-button):hover,
  .vs-header-bar__nav a[aria-current="page"] {
    color: var(--vs-ink);
  }
  .vs-header-bar__nav a[aria-current="page"] {
    text-decoration: underline;
    text-decoration-color: var(--vs-accent);
    text-decoration-thickness: 2px;
    text-underline-offset: 6px;
  }

  /* Folded menu (narrow) */
  .vs-header-bar__menu summary {
    display: flex;
    align-items: center;
    gap: 10px;
    height: 36px;
    padding: 0 14px;
    border: var(--vs-border-w) solid var(--vs-line-strong);
    border-radius: var(--vs-radius-pill);
    font-size: 0.875rem;
    font-weight: 500;
    cursor: pointer;
    list-style: none;
  }
  .vs-header-bar__menu summary::-webkit-details-marker {
    display: none;
  }
  .vs-header-bar__burger {
    display: grid;
    gap: 4px;
  }
  .vs-header-bar__burger i {
    display: block;
    width: 14px;
    height: 1.5px;
    background: currentColor;
    transition: transform var(--vs-duration) var(--vs-ease-out);
  }
  .vs-header-bar__menu[open] .vs-header-bar__burger i:first-child {
    transform: translateY(2.75px) rotate(45deg);
  }
  .vs-header-bar__menu[open] .vs-header-bar__burger i:last-child {
    transform: translateY(-2.75px) rotate(-45deg);
  }
  .vs-header-bar__sheet {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    display: grid;
    gap: 20px;
    padding: 12px var(--vs-gutter) 24px;
    border-bottom: var(--vs-border-w) solid var(--vs-line);
    background: var(--vs-page);
    box-shadow: var(--vs-shadow-lg);
    animation: vs-fade-in var(--vs-duration) var(--vs-ease-out);
  }
  .vs-header-bar__sheet li a {
    display: block;
    padding: 12px 0;
    border-bottom: 1px solid var(--vs-line);
    font-size: 1.0625rem;
  }

  @container (min-width: 820px) {
    .vs-header-bar__nav {
      display: flex;
    }
    .vs-header-bar__menu {
      display: none;
    }
  }
</style>
```
