UI Library
Site header
A sticky marketing header with a translucent blur, inline nav from the md breakpoint up, and a real disclosure menu below it. The mobile panel is toggled by a button that reports its own state, not a bare icon.
Navigationheadernavstickyresponsive
Preview
apps/site/blocks/components/SiteHeader.tsx
'use client';
import { useState } from 'react';
import { Menu, X } from 'lucide-react';
export interface NavLink {
label: string;
href: string;
}
const DEFAULT_LINKS: NavLink[] = [
{ label: 'Product', href: '#' },
{ label: 'Pricing', href: '#' },
{ label: 'Docs', href: '#' },
{ label: 'About', href: '#' },
];
export function SiteHeader({
brand = 'Acme',
links = DEFAULT_LINKS,
ctaLabel = 'Get started',
ctaHref = '#',
}: {
brand?: string;
links?: NavLink[];
ctaLabel?: string;
ctaHref?: string;
}) {
const [open, setOpen] = useState(false);
return (
<header className="sticky top-0 z-40 w-full border-b border-border/60 bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4 sm:px-6">
<a href="#" className="flex items-center gap-2 text-sm font-semibold tracking-tight">
<span className="flex h-6 w-6 items-center justify-center rounded bg-primary text-xs font-bold text-primary-foreground">
{brand.charAt(0)}
</span>
{brand}
</a>
<nav aria-label="Main" className="hidden items-center gap-6 text-sm text-muted-foreground md:flex">
{links.map((link) => (
<a key={link.label} href={link.href} className="transition-colors hover:text-foreground">
{link.label}
</a>
))}
</nav>
<div className="hidden md:block">
<a
href={ctaHref}
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
>
{ctaLabel}
</a>
</div>
{/* aria-expanded + aria-controls, so the button announces what it does
and what it owns — an unlabelled icon button tells a screen reader
nothing. */}
<button
type="button"
aria-expanded={open}
aria-controls="site-header-mobile"
aria-label={open ? 'Close menu' : 'Open menu'}
onClick={() => setOpen((o) => !o)}
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground md:hidden"
>
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
{open ? (
<div id="site-header-mobile" className="border-t border-border/60 md:hidden">
<nav aria-label="Main" className="mx-auto max-w-6xl space-y-1 px-4 py-3 sm:px-6">
{links.map((link) => (
<a
key={link.label}
href={link.href}
onClick={() => setOpen(false)}
className="block rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
{link.label}
</a>
))}
<a
href={ctaHref}
className="mt-2 inline-flex h-9 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground"
>
{ctaLabel}
</a>
</nav>
</div>
) : null}
</header>
);
}
Dependencies
lucide-react— npm package
Colours come from CSS variables (--background, --primary, --border) using the shadcn/ui token names, so the block adopts your palette rather than importing ours.