Website Components
Header Bar.
Brand left, links and a call to action right. Folds into a native details menu when narrow. No JavaScript.
Site chromeSection1 dependency Original, MIT
Style
Using Claude Code? Give it the agent prompt and skip these steps.
-
Import the foundation once
foundation.css holds the tokens every component reads.
--- // src/layouts/Layout.astro import "../styles/foundation.css"; --- -
Use it
--- import HeaderBar from "../components/viewership/header-bar/HeaderBar.astro"; --- <HeaderBar />
Props 7
| 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 1 file
---
/**
* 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> Original, MIT.