UI.MD

Site Header

The shared site chrome header: brand, nav, and utility slots, with the layout decisions exposed as props.

Installation

$
npx shadcn@latest add https://ui.mattdowney.com/r/site-header.json

Usage

import { SiteHeader } from "@/components/ui/site-header"
import { Nav, NavItem, NavList } from "@/components/ui/nav"

Examples

Default

Nav beside the brand, sticky, hairlined, and transparent until content scrolls under it. The blur needs real page scroll, so the bar shows here at rest. Click between the nav items: the selected pill is Nav's, not the header's.

UI.MD

Page title

The bar spans the full width; its contents sit on the shared chrome width, the same one the footer uses.

Centered nav

A three-column grid holds the nav on the bar's true center, so it stays put when a label changes or a badge appears beside the utilities.

UI.MD

Page title

The bar spans the full width; its contents sit on the shared chrome width, the same one the footer uses.

Solid, not sticky

A static bar that scrolls away with the page. The scroll listener is skipped entirely.

UI.MD

Page title

The bar spans the full width; its contents sit on the shared chrome width, the same one the footer uses.

No hairline

For a bar running flush into a section that shares its surface, where a line would only announce a seam. This is an edge decision and nothing more; artwork is the hero surface below. The page decides it, not the header.

UI.MD

A flush section

The bar and the section under it share one surface, so a line between them would only announce a seam nobody needs to see.

Over a hero image

surface="hero" lays a masked scrim (tint plus blur) behind the chrome and makes the bar a dark island, so the nav's selected pill and every other component inside resolve against dark tokens whatever the page is set to. Over a photograph the page's light/dark mode is not the background, the photograph is. The hairline defaults off here: the scrim's falloff is the edge. The page positions the bar over the art; the shell never takes over layout.

UI.MD

The bar sits on the art

The scrim darkens whatever runs under the chrome. The bright band on the left and the near-black one beside it get the same treatment, so nothing in the bar has to know what it landed on.

API Reference

PropTypeDefaultDescription
brandSiteHeaderReactNode-Start of the bar, usually a home link wrapping <Brand />.
navSiteHeaderReactNode-Primary navigation. Put the kit's <Nav /> here: it is the primitive for a top nav and brings the sliding selected pill, the control-scale sizes, and its own <nav> landmark. siteHeaderNavItemClass is the quieter alternative, colour-only links with no fill and no indicator, for chrome that should ship no client JS; wrap those in your own <nav>, since the slot wrapper is a plain div. Hidden below md, where sites bring their own drawer.
childrenSiteHeaderReactNode-Right-side utilities: theme toggle, search, account.
navPlacementSiteHeader"start" | "center""start"start keeps the nav beside the brand with a spacer absorbing the rest. center switches to a three-column grid that holds the nav on the true center regardless of how wide the brand and utilities are.
stickySiteHeaderbooleantruePins the bar to the top of the viewport at z-40.
surfaceSiteHeader"scroll-translucent" | "solid" | "transparent" | "hero""scroll-translucent"scroll-translucent is transparent at rest and blurs once content scrolls under it (needs the --background-translucent token). solid and transparent are static and skip the scroll listener. hero is for a bar sitting on artwork: a top-down scrim plus light content in both themes. Pair hero with sticky={false}, or with a page that swaps to solid on scroll, since a scrim travelling over body copy is a bug.
hairlineSiteHeaderboolean`true`, or `false` under `surface="hero"`The bottom border. Turn it off for a bar running flush into a section that shares its surface. Under hero it defaults off because the scrim's falloff is already the edge; passing it explicitly still wins in both directions.
classNameSiteHeaderstring-Merged onto the <header>, not the inner chrome container.