Site header
A responsive site header with a brand link, primary links with one level of dropdowns, a call to action, optional actions and a small-screen drawer.
cmp_site_header_01 Preview
Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.
Use this component with your coding agent
Using the PageSugar MCP server, fetch component cmp_site_header_01 version 1.0.0 with variant "neutral", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.
Code
Artifact sha256-891bf3b5…bd7b57fc
sha256-891bf3b5c07f2a6dc4c9fe82848878cbb23d641716851ba383d9be74bd7b57fcThis component needs all 4 files. Download the ZIP
/** A single destination in the primary navigation. */
export interface NavLink {
label: string;
href: string;
/** Optional one-line description shown under the label inside a dropdown. */
description?: string;
}
/** A top-level item that opens a dropdown of related links. */
export interface NavGroup {
label: string;
/** Optional overview page for the group, listed first inside the dropdown. */
href?: string;
children: NavLink[];
}
export type NavItem = NavLink | NavGroup;
export interface Brand {
/** Site name; the brand link's accessible name and its fallback text. */
name: string;
href: string;
}
export interface HeaderLink {
label: string;
href: string;
}
export interface HeaderLabels {
/** Text of the small-screen menu button. */
menu: string;
/** Accessible name of the drawer's close button. */
close: string;
/** Visually hidden suffix on a dropdown whose links include the current page. */
currentSection: string;
}
export const isGroup = (item: NavItem): item is NavGroup =>
'children' in item && Array.isArray(item.children);
/**
* Items as rendered: a group with no children becomes a plain link when it has an href and
* is dropped otherwise, so nothing renders as a button with an empty panel.
*/
export const renderableItems = (items: NavItem[]): NavItem[] =>
items.flatMap((item) => {
if (!isGroup(item) || item.children.length > 0) return [item];
return item.href ? [{ label: item.label, href: item.href }] : [];
});
const normalizePath = (path: string) => {
const bare = path.split(/[?#]/, 1)[0];
return bare.length > 1 ? bare.replace(/\/+$/, '') : bare;
};
/** True when a link href names the current pathname, ignoring query, hash and a trailing slash. */
export const isCurrentPath = (href: string | undefined, currentPath: string | undefined) =>
Boolean(href && currentPath) && normalizePath(href!) === normalizePath(currentPath!);
/** True when the group's own page or one of its children is the current page. */
export const isCurrentGroup = (group: NavGroup, currentPath: string | undefined) =>
isCurrentPath(group.href, currentPath) ||
group.children.some((child) => isCurrentPath(child.href, currentPath));
Usage #
On this pageSupply the brand, items and an optional call to action; pass the current pathname so the matching link is marked. Dropdown and drawer state is local. The header does not read the router, does not handle search, account or cart state (put those in the actions snippet), and supports one level of dropdowns only.
- Suggested location
src/lib/components/site-header-01
Limitations
- Before hydration the
small-screenmenu control is an anchor that reveals a flat list of the navigation links (not the actions snippet); dropdown buttons on wide screens open only once JavaScript has run. currentPathis compared with each href as a pathname (query, hash and a trailing slash are ignored); absolute URLs never match, and two items that normalise to the same pathname are both marked.- A group with no children renders as a plain link when it has an href and is omitted otherwise.
- The breakpoint is the lg: utilities in the source plus one matching media query, not a prop; the actions snippet joins the bar at sm and is always in the drawer footer.
- The actions snippet renders in the header bar from sm up and in the drawer footer at every width below lg, so it is mounted twice at once.
- Dark colours apply inside an ancestor with the class dark; a media-query setup needs its own rule that sets the
--site-header-* variables.
Example
<!-- Illustrative content: replace the links and labels with your own routes. -->
<script lang="ts">
import { page } from '$app/state';
import SiteHeader from '$lib/components/site-header-01/SiteHeader.svelte';
import type { NavItem } from '$lib/components/site-header-01/types';
const items: NavItem[] = [
{
label: 'Product',
children: [
{ label: 'Features', href: '/product/features', description: 'What the product does.' },
{ label: 'Security', href: '/product/security' }
]
},
{ label: 'Pricing', href: '/pricing' },
{ label: 'Docs', href: '/docs' }
];
</script>
<SiteHeader
brand={{ name: 'Example', href: '/' }}
{items}
cta={{ label: 'Get started', href: '/signup' }}
currentPath={page.url.pathname}
>
{#snippet logo()}
<img src="/logo.svg" alt="" width="28" height="28" />
<span>Example</span>
{/snippet}
{#snippet actions()}
<a href="/signin" class="text-sm font-medium">Sign in</a>
{/snippet}
</SiteHeader>Adding the complete file set #
Copy all four files into src/lib/components/site-header-01/, keeping these relative paths:
SiteHeader.svelte
parts/MobileDrawer.svelte
parts/NavDropdown.svelte
types.tsThe entry imports both parts and the types file, so copying it alone does not compile. There are no package dependencies.
Marking the current page #
The header never reads the router. Pass the current pathname yourself, for example page.url.pathname from $app/state in SvelteKit. The link whose href matches it exactly (query, hash and a trailing slash are ignored) gets aria-current="page". Keep destinations unique within the items: two links that normalise to the same pathname would both be marked. In the bar that link, or the dropdown whose group href or child matches, carries a 2 px accent rule sitting on the bar's bottom hairline; the dropdown also gets a visually hidden (current section) suffix, so the section is announced without a second aria-current. Inside the panel and the drawer the current link is marked by a fill and a heavier weight.
Before JavaScript runs #
Below the breakpoint the menu control is server-rendered as an anchor to a hidden list of every navigation link (the items, flattened), which the browser reveals through :target. After hydration the anchor becomes the menu button and that list is replaced by the drawer; a visitor who had already opened the list lands in the open drawer. The actions snippet is not part of that list, so below sm its links are unavailable until JavaScript runs. Above the breakpoint the inline links work immediately, but dropdown buttons only open once the component has hydrated.
Sticky headers #
sticky pins the header with a translucent, blurred surface. Because it now covers the top of the viewport, add scroll padding so anchors and focused elements are not hidden under it:
html {
scroll-padding-top: 4rem;
}The bar is 4rem tall when its links fit on one row; measure it if your labels wrap.
Moving the breakpoint #
Inline links show from lg (64rem) and the menu button below it. To change that, replace every lg: utility in SiteHeader.svelte with the breakpoint you want, and change the (min-width: 64rem) query in its onMount to match, so the drawer still closes when the viewport grows past the new breakpoint. The actions snippet shows in the bar from sm (sm:flex) and always in the drawer footer, because the open drawer covers the bar. When the brand, the call to action and Menu do not fit on one line below the breakpoint (long labels, a narrow phone), the header measures that and gives the call to action a full-width row of its own under the brand; until JavaScript runs the labels wrap instead.
Colours and dark mode #
Four variables recolour everything; set them on any ancestor:
:root {
--site-header-surface: #0f172a;
--site-header-text: #f8fafc;
--site-header-accent: #f8fafc;
--site-header-on-accent: #0f172a;
}The hairline, the two muted text tones, the hover fills and the shadow rings are mixed from the text colour, and the primary action's hover tone from the accent, so they all follow the four variables. An ancestor with the class dark switches the built-in fallbacks to a dark surface. If your site uses prefers-color-scheme instead, set the four variables inside your own media query.
Other languages #
navLabel names the nav landmarks and labels replaces the menu button text, the drawer's close label and the current-section suffix. Text direction comes from the document: set dir="rtl" on <html> and the layout, dropdown alignment and drawer side follow.
Props and content inputs #
On this page| Name | Type | Required | Default | Description |
|---|---|---|---|---|
brand | Brand | Yes | None | { name, href }. The site name is the brand link’s accessible name and its text when no logo snippet is given. |
items | NavItem[] | Yes | None | Primary items in order: { label, href, description? } for a link, or { label, href?, children: NavLink[] } for a dropdown. A group href is listed first inside its dropdown; a group with no children renders as a link or is omitted. |
logo | Snippet | No | None | Logo markup rendered inside the brand link (inline SVG, img with empty alt, or text). Decorative: the link is named by brand.name. |
currentPath | string | No | None | Current URL pathname, for example page.url.pathname. The exact match gets aria-current="page"; a dropdown containing it gets a visible indicator. |
cta | HeaderLink | No | None | { label, href }. Primary action link shown at every width and repeated in the drawer. |
actions | Snippet | No | None | Extra actions such as a sign-in link or mini cart. Shown in the bar from sm up and always in the drawer footer. |
sticky | boolean | No | false | Sticks the header to the top with a translucent, blurred surface. Add scroll-padding-top to html so anchors are not hidden under it. |
align | 'center' | 'end' | No | 'end' | Where the link list sits between the brand and the actions on wide screens. |
navLabel | string | No | 'Main' | Accessible name of the nav landmarks (inline list, fallback list and drawer). |
labels | Partial<HeaderLabels> | No | { menu: 'Menu', close: 'Close menu', currentSection: '(current section)' } | Text of the menu button, the drawer close button’s accessible name and the hidden suffix on a current dropdown. Override for other languages. |
Customization #
On this pageChange content through props and snippets, recolour through four CSS variables on any ancestor, and edit the lg: utilities in the source (with the matching media query) to move the breakpoint.
- Colours: set
--site-header-surface,--site-header-text,--site-header-accentand--site-header-on-accenton an ancestor. Keep text on surface and on-accent on accent at 4.5:1 or better; the hairline, the secondary text tone, the hover, selected and pressed fills and the shadow rings are mixed from the text colour, and the accent hover is mixed from the accent. - Inverted header: the inverted palette swaps the surface and text fallbacks; setting the four variables achieves the same on any page.
- Dark mode: an ancestor with the class dark switches the fallbacks. For a prefers-color-scheme setup, add a media query that sets the four variables.
- Breakpoint: the inline list uses hidden
lg:flexand the menu buttonlg:hidden; change every lg: utility inSiteHeader.sveltetogether with the (min-width: 64rem) query in itsonMount. The actions snippet switches between the bar and the drawer withsm:flexandsm:hidden; keep that below the navigation breakpoint. - Sticky: pass sticky for a translucent, blurred bar (80% surface over
backdrop-blur-md) and add html { scroll-padding-top: 4rem } so focused elements and anchor targets are not hidden under it. - Width: the bar is capped at
max-w-7xlon the inner wrapper; change it there. - Dropdown panel: width and alignment live in parts/NavDropdown.svelte (
w-72,end-0orstart-0); it is the one elevated surface, with the layered popover shadow declared as --_shadow-popover inSiteHeader.svelte; descriptions render when a child has one. - Drawer: width and side are the
max-w-smandend-0utilities on the dialog in parts/MobileDrawer.svelte. - Links: every href is rendered as given; pass real routes and keep labels short enough for one row at 1280 px, or accept a wrapped second row.
Public CSS variables
| Variable | Token |
|---|---|
--site-header-surface | surface |
--site-header-text | text |
--site-header-accent | accent |
--site-header-on-accent | onAccent |
Accessibility #
On this page- A header landmark containing the brand link and a nav landmark named by
navLabel; the drawer holds a second nav with the same name, and the pre-hydration fallback list a third, only one of which is displayed at a time. - Dropdowns follow the APG disclosure navigation pattern: a button with
aria-expandedandaria-controls, a plain link list, no menu roles. Enter or Space toggles, Escape closes and returns focus to the button, Down Arrow opens and enters the list, Up and Down move between links, focus leaving or a press outside closes it. Opening one closes the others. - The exact
currentPathmatch getsaria-current="page" (once per navigation region when destinations are unique). In the bar the current link, or the dropdown containing the current page, carries a 2 px accent rule on the bar’s bottom hairline plus a visually hiddencurrentSectionsuffix on the dropdown; in the panel and the drawer the current link is marked by a fill and a heavier weight. - The
small-screendrawer is a native modal dialog labelled by the menu label: focus moves into it on open, Escape or a press on the scrim closes it, and focus returns to the menu button on close. Activating any link inside it, including links from the actions snippet, closes it. - The brand link is named by
brand.namethrougharia-labelwhen a logo snippet is supplied, so logo images should carry empty alt text. - Built-in links and buttons show a 2 px focus outline in the accent colour with an offset, visible on both palettes. Controls you place in the logo and actions snippets need their own focus styles and accessible names, and any ids inside the actions snippet must stay unique across its two mounted copies.
- IDs come from
$props.id(), so two headers on one page keep distinctaria-controlstargets and dialog ids.
Known limitations
- On wide screens the dropdown buttons do nothing until JavaScript runs; the fallback list only exists below the breakpoint.
- Hover does not open a dropdown; it opens on click and keyboard only.
- The drawer is not a focus-trapping custom widget; it relies on the browser’s native modal dialog, which needs a browser with <dialog> support.
Release details #
On this page- Integration
- Local interaction
- Requires client-side JavaScript to be interactive
- Server-side rendering supported
- Dependencies
- No additional runtime packages beyond Svelte and Tailwind CSS
- License
MIT. Default license approval is pending; see the license status before adopting the source.
- Version history
- 1.0.0 Published Current release · 17 September 2026
Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.