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.

Inside a padded page container

The shell inside a page column that pads itself. The first bar draws its border on the shell, so the line stops short of both page edges. The second passes bleed, and the rule escapes the padding to meet them while nothing in the bar moves. hairline decides whether an edge exists, bleed only where it is drawn.

UI.MD

The border is on the shell, so it stops where the container's padding starts.

UI.MD

The rule is on its own box, pulled out past the padding. The bar itself did not move.

Over a hero image

surface="hero" lays a masked scrim behind the chrome and turns the bar into a dark island, so the nav's pill and everything else inside resolve against dark tokens whatever the page is set to. The hairline defaults off here: the scrim's falloff is the edge. Positioning the bar over the art stays the page's job.

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.
hairlineSiteHeaderbooleantrueWhether there is a bottom edge. 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.
bleedSiteHeaderboolean | stringfalseWhere that edge is drawn. The default is the full-width mount: the border sits on the shell, which is correct when the shell already spans the viewport. Set it for a bar inside a page container that pads itself, and the rule moves to a full-width child that escapes the padding by a negative inline margin, so it meets the page's real edges while the bar's contents stay put. true reads the distance from --spacing-chrome-bleed (which a responsive container redeclares at its own breakpoint); a string states it directly. A no-op wherever there is no hairline, including surface="hero".
containerSiteHeaderbooleantrueWhether the shell builds its own centred container. On by default, which is right when the bar spans the viewport: it centres the row at --container-chrome and pads it with --spacing-chrome-gutter. Set it to false when the bar already sits inside a page container that centres and pads, or the two nest and the bar's contents sit inboard of the rest of the page by a full gutter. The alternative, neutralising both tokens, is global: it moves every shell on the site, so a page with one nested bar and one full-width bar cannot express both. Orthogonal to bleed: that moves the edge past a container's padding, this declines to build the container at all.
classNameSiteHeaderstring-Merged onto the <header>, not the inner chrome container.