Header
blockmarketingSite header collection: sticky top bar, dismissible announcement stack, centered-logo split, floating pill, full-width mega menu, inline search with command palette, and a transparent-over-hero scroll variant — each with a mobile sheet fallback.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/header.json$npx shadcn@latest add https://uipkge.dev/r/lit/header.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/header.json$bunx shadcn@latest add https://uipkge.dev/r/lit/header.jsonManifest:
https://uipkge.dev/r/lit/header.jsonInstalls to:src/uip/blocks/header/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | HeaderVariant | default | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
MegaIteminterface MegaItem {
icon: IconNode
iconName: string
title: string
body: string
}MegaMenuinterface MegaMenu {
label: string
groups: { heading: string; items: MegaItem[] }[]
highlight: { title: string; body: string; cta: string }
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-header': UipHeader
}
}Files installed (1)
src/uip/blocks/header/header.ts33.9 kB
import { LitElement, css, html, isServer, nothing } from 'lit' import { ArrowRight, BarChart3, BookOpen, Boxes, CircleDot, Compass, FileText, History, Layers, LifeBuoy, Lock, Menu, Search, ShoppingBag, User, Users, Workflow, X, type IconNode, } from 'lucide' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/badge/badge' import '../../components/button/button' import '../../components/command/command' import '../../components/kbd/kbd' import '../../components/navigation-menu/navigation-menu' import '../../components/separator/separator' import '../../components/sheet/sheet' export type HeaderVariant = | 'default' | 'announcement' | 'centered' | 'pill' | 'mega' | 'search' | 'transparent' const STORAGE_KEY = 'uipkge:announcement-dismissed' const defaultLinks = [ { label: 'Features', href: '#features' }, { label: 'Pricing', href: '#pricing' }, { label: 'Customers', href: '#customers' }, { label: 'Docs', href: '#docs' }, { label: 'Blog', href: '#blog' }, ] const stackLinks = [ { label: 'Platform', href: '#platform' }, { label: 'Pricing', href: '#pricing' }, { label: 'Customers', href: '#customers' }, { label: 'Docs', href: '#docs' }, ] const centeredLeft = [ { label: 'Collections', href: '#collections' }, { label: 'New in', href: '#new' }, { label: 'Studio', href: '#studio' }, ] const centeredRight = [ { label: 'Journal', href: '#journal' }, { label: 'Stockists', href: '#stockists' }, { label: 'Contact', href: '#contact' }, ] const centeredLinkClass = 'text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none' const pillLinks = [ { label: 'Product', href: '#product' }, { label: 'Pricing', href: '#pricing' }, { label: 'Customers', href: '#customers' }, { label: 'Docs', href: '#docs' }, ] interface MegaItem { icon: IconNode iconName: string title: string body: string } interface MegaMenu { label: string groups: { heading: string; items: MegaItem[] }[] highlight: { title: string; body: string; cta: string } } // `iconName` beside each icon node: lucide nodes don't carry their kebab-case // name, which icon() needs for the lucide-<name> class. const megaMenus: MegaMenu[] = [ { label: 'Product', groups: [ { heading: 'Core', items: [ { icon: Boxes, iconName: 'boxes', title: 'Metric layer', body: 'Versioned definitions your whole org shares.' }, { icon: BarChart3, iconName: 'chart-column', title: 'Dashboards', body: 'Composed from certified metrics, not raw SQL.', }, { icon: Workflow, iconName: 'workflow', title: 'Pipelines', body: 'Scheduled materialisation with cost ceilings.' }, ], }, { heading: 'Governance', items: [ { icon: Lock, iconName: 'lock', title: 'Row-level access', body: 'Scope evaluated per query, per identity.' }, { icon: BookOpen, iconName: 'book-open', title: 'Change history', body: 'Every definition edit reviewed and revertible.', }, { icon: LifeBuoy, iconName: 'life-buoy', title: 'Audit exports', body: 'Generated from the same history, not by hand.', }, ], }, ], highlight: { title: 'Ship your first dashboard today', body: 'Connect a warehouse, publish one certified metric, and invite the team. No migration required.', cta: 'Start the walkthrough', }, }, { label: 'Resources', groups: [ { heading: 'Learn', items: [ { icon: BookOpen, iconName: 'book-open', title: 'Documentation', body: 'Setup, modelling, and the access model.', }, { icon: Layers, iconName: 'layers', title: 'Guides', body: 'Reconciliation and first-close checklists.' }, { icon: BarChart3, iconName: 'chart-column', title: 'Benchmarks', body: 'Close speed across 240 finance teams.', }, ], }, { heading: 'Support', items: [ { icon: LifeBuoy, iconName: 'life-buoy', title: 'Help centre', body: 'Answers to the questions support gets most.', }, { icon: Workflow, iconName: 'workflow', title: 'Status', body: 'Live uptime and incident history.' }, { icon: Lock, iconName: 'lock', title: 'Trust centre', body: 'SOC 2, subprocessors, and data residency.' }, ], }, ], highlight: { title: 'Read the rollout plan', body: 'The five-week implementation we run for every customer, written down so you can run it yourself.', cta: 'Open the plan', }, }, ] const megaPlainLinks = [ { label: 'Pricing', href: '#pricing' }, { label: 'Customers', href: '#customers' }, ] const searchLinks = [ { label: 'Product', href: '#product' }, { label: 'Pricing', href: '#pricing' }, { label: 'Docs', href: '#docs' }, ] const searchGroups: { heading: string; items: { icon: IconNode; iconName: string; label: string }[] }[] = [ { heading: 'Documentation', items: [ { icon: BookOpen, iconName: 'book-open', label: 'Connect a warehouse' }, { icon: FileText, iconName: 'file-text', label: 'Define your first metric' }, { icon: BarChart3, iconName: 'chart-column', label: 'Publish a dashboard' }, ], }, { heading: 'Company', items: [ { icon: Users, iconName: 'users', label: 'Customer stories' }, { icon: History, iconName: 'history', label: 'Changelog' }, ], }, ] const transparentLinks = [ { label: 'Product', href: '#product' }, { label: 'Solutions', href: '#solutions' }, { label: 'Pricing', href: '#pricing' }, { label: 'Docs', href: '#docs' }, ] /** * <uip-header> — the registry Header block collection as a web component. * * One block, one install unit: `variant` selects the section * ('default' | 'announcement' | 'centered' | 'pill' | 'mega' | 'search' | * 'transparent'). Class strings are React's verbatim per variant. React state * (useState) becomes @state fields; mobile Sheets become <uip-sheet> (open + * open-change), the mega dropdowns become <uip-navigation-menu>, and the * search palette becomes <uip-command-dialog>. Timers, scroll / keydown * listeners and the pill ResizeObserver are all torn down in * disconnectedCallback. */ export class UipHeader extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { variant: { reflect: true }, sheetOpen: { state: true }, dismissed: { state: true }, pillActive: { state: true }, indicator: { state: true }, searchOpen: { state: true }, scrolled: { state: true }, } variant: HeaderVariant = 'default' private sheetOpen = false private dismissed = false private pillActive = 0 private indicator = { left: 0, width: 0 } private searchOpen = false private scrolled = false private observer?: ResizeObserver constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'header') if (isServer) return try { if (window.sessionStorage.getItem(STORAGE_KEY) === '1') this.dismissed = true } catch { // Blocked storage: the strip stays visible, like React's first paint. } window.addEventListener('scroll', this.onScroll, { passive: true }) this.onScroll() window.addEventListener('keydown', this.onKeydown) } disconnectedCallback() { super.disconnectedCallback() window.removeEventListener('scroll', this.onScroll) window.removeEventListener('keydown', this.onKeydown) this.observer?.disconnect() this.observer = undefined } firstUpdated() { this.measurePill() if (typeof ResizeObserver === 'undefined') return this.observer = new ResizeObserver(() => this.measurePill()) const nav = this.renderRoot.querySelector('[data-pill-nav]') if (nav) this.observer.observe(nav) } private onScroll = () => { this.scrolled = window.scrollY > 24 } private onKeydown = (e: KeyboardEvent) => { if (e.key.toLowerCase() !== 'k' || !(e.metaKey || e.ctrlKey)) return e.preventDefault() this.searchOpen = !this.searchOpen } private dismiss() { this.dismissed = true try { window.sessionStorage.setItem(STORAGE_KEY, '1') } catch { // Blocked storage: the dismissal still holds for this page view. } } // Measured from the live element, so the indicator fits any label length or // font — no hard-coded widths to drift out of sync with the copy. private measurePill(index = this.pillActive) { const links = this.renderRoot.querySelectorAll<HTMLElement>('[data-pill-link]') const el = links[index] if (el) this.indicator = { left: el.offsetLeft, width: el.offsetWidth } } private mobileSheet(heading: string, body: unknown, side: 'right' | 'left' | 'top' = 'right') { return html` <uip-sheet side=${side} heading=${heading} .open=${this.sheetOpen} @open-change=${(e: CustomEvent<{ open: boolean }>) => { this.sheetOpen = e.detail.open }} > ${body} </uip-sheet> ` } render() { switch (this.variant) { case 'announcement': return this.renderAnnouncement() case 'centered': return this.renderCentered() case 'pill': return this.renderPill() case 'mega': return this.renderMega() case 'search': return this.renderSearch() case 'transparent': return this.renderTransparent() default: return this.renderDefault() } } private renderDefault() { return html` <header data-slot="header-01" class="bg-background/80 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-40 border-b backdrop-blur" > <div class="mx-auto flex h-16 max-w-6xl items-center justify-between px-6"> <a href="#" class="flex items-center gap-2"> <div class="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md"> ${icon(Boxes, 'boxes', 'size-4')} </div> <span class="text-base font-semibold">Acme</span> </a> <nav class="hidden items-center gap-6 md:flex" aria-label="Primary"> ${defaultLinks.map( (link) => html`<a href=${link.href} class="text-muted-foreground hover:text-foreground text-sm transition-colors" > ${link.label} </a>`, )} </nav> <div class="hidden items-center gap-2 md:flex"> <uip-button variant="ghost" size="sm">Sign in</uip-button> <uip-button size="sm">Start free trial</uip-button> </div> <div class="md:hidden"> ${this.mobileSheet( 'Acme', html` <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open menu"> ${icon(Menu, 'menu', 'size-5')} </uip-button> <nav class="flex flex-1 flex-col gap-1 p-4" aria-label="Mobile"> ${defaultLinks.map( (link) => html`<a href=${link.href} class="hover:bg-muted rounded-md px-3 py-2 text-sm transition-colors" @click=${() => { this.sheetOpen = false }} > ${link.label} </a>`, )} </nav> <div class="flex flex-col gap-2 border-t p-4"> <uip-button variant="outline" class="w-full">Sign in</uip-button> <uip-button class="w-full">Start free trial</uip-button> </div> `, )} </div> </div> </header> ` } private renderAnnouncement() { return html` <div data-slot="header-announcement-stack" class="sticky top-0 z-40"> ${!this.dismissed ? html` <div class="border-border bg-muted/60 border-b backdrop-blur"> <div class="mx-auto flex h-10 max-w-6xl items-center gap-3 px-6 text-sm"> <uip-badge variant="secondary" class="shrink-0">New</uip-badge> <p class="min-w-0 truncate"> Row-level scoping now evaluates against SCIM groups. <a href="#changelog" class="hover:text-foreground underline underline-offset-4"> Read the changelog </a> </p> ${icon(ArrowRight, 'arrow-right', 'text-muted-foreground hidden size-3.5 shrink-0 sm:block')} <uip-button variant="ghost" size="icon" class="ml-auto size-7 shrink-0" aria-label="Dismiss announcement" @click=${this.dismiss} > ${icon(X, 'x', 'size-3.5')} </uip-button> </div> </div> ` : ''} <header class="border-border bg-background/90 border-b backdrop-blur"> <div class="mx-auto flex h-16 max-w-6xl items-center gap-6 px-6"> <a href="#top" class="flex shrink-0 items-center gap-2 font-semibold tracking-tight"> ${icon(Layers, 'layers', 'text-primary size-5')} Northwind </a> <nav class="hidden items-center gap-1 md:flex" aria-label="Primary"> ${stackLinks.map( (link) => html`<a href=${link.href} class="text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:ring-ring rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none" > ${link.label} </a>`, )} </nav> <div class="ml-auto hidden items-center gap-2 md:flex"> <uip-button variant="ghost" size="sm">Sign in</uip-button> <uip-button size="sm">Book a demo</uip-button> </div> <div class="ml-auto md:hidden"> ${this.mobileSheet( 'Menu', html` <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open navigation menu"> ${icon(Menu, 'menu', 'size-5')} </uip-button> <nav class="mt-6 grid gap-1" aria-label="Mobile"> ${stackLinks.map( (link) => html`<a href=${link.href} class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors" @click=${() => { this.sheetOpen = false }} > ${link.label} </a>`, )} </nav> <uip-separator class="[&::part(base)]:my-5"></uip-separator> <div class="grid gap-2"> <uip-button variant="outline">Sign in</uip-button> <uip-button>Book a demo</uip-button> </div> `, )} </div> </div> </header> </div> ` } private renderCentered() { return html` <header data-slot="header-centered-logo" class="border-border bg-background border-b"> <div class="mx-auto flex h-16 max-w-6xl items-center px-6"> <nav class="hidden flex-1 basis-0 items-center gap-1 lg:flex" aria-label="Primary left"> ${centeredLeft.map( (link) => html`<a href=${link.href} class=${centeredLinkClass}>${link.label}</a>`, )} </nav> <div class="lg:hidden"> ${this.mobileSheet( 'Menu', html` <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open navigation menu"> ${icon(Menu, 'menu', 'size-5')} </uip-button> <nav class="mt-6 grid gap-1" aria-label="Mobile"> ${[...centeredLeft, ...centeredRight].map( (link) => html`<a href=${link.href} class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors" @click=${() => { this.sheetOpen = false }} > ${link.label} </a>`, )} </nav> <uip-separator class="[&::part(base)]:my-5"></uip-separator> <uip-button class="w-full">Account</uip-button> `, 'left', )} </div> <a href="#top" class="mx-auto flex items-center gap-2 text-base font-semibold tracking-[0.18em] uppercase lg:mx-6" > ${icon(Compass, 'compass', 'text-primary size-4')} Halden </a> <nav class="hidden flex-1 basis-0 items-center justify-end gap-1 lg:flex" aria-label="Primary right"> ${centeredRight.map( (link) => html`<a href=${link.href} class=${centeredLinkClass}>${link.label}</a>`, )} </nav> <div class="flex items-center gap-1 lg:ml-4"> <uip-button variant="ghost" size="icon" aria-label="Account"> ${icon(User, 'user', 'size-4')} </uip-button> <uip-button variant="ghost" size="icon" aria-label="Bag"> ${icon(ShoppingBag, 'shopping-bag', 'size-4')} </uip-button> </div> </div> </header> ` } private renderPill() { return html` <div data-slot="header-floating-pill" class="pointer-events-none sticky top-0 z-40 px-4 pt-4"> <header class="border-border bg-card/85 pointer-events-auto mx-auto flex h-14 max-w-4xl items-center gap-3 rounded-full border px-3 shadow-sm backdrop-blur" > <a href="#top" class="flex shrink-0 items-center gap-2 pl-2 font-semibold tracking-tight"> ${icon(CircleDot, 'circle-dot', 'text-primary size-4')} Verity </a> <nav data-pill-nav class="relative hidden items-center md:flex" aria-label="Primary"> <span class="bg-muted absolute inset-y-1 rounded-full transition-colors duration-200 ease-out" style=${`left: ${this.indicator.left}px; width: ${this.indicator.width}px`} aria-hidden="true" ></span> ${pillLinks.map( (link, index) => html`<a href=${link.href} data-pill-link class=${`focus-visible:ring-ring relative rounded-full px-3.5 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none ${ index === this.pillActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground' }`} aria-current=${index === this.pillActive ? 'page' : nothing} @mouseenter=${() => this.measurePill(index)} @mouseleave=${() => this.measurePill()} @focus=${() => this.measurePill(index)} @blur=${() => this.measurePill()} @click=${() => { this.pillActive = index }} > ${link.label} </a>`, )} </nav> <uip-button size="sm" class="ml-auto [&::part(base)]:rounded-full">Start free</uip-button> <div class="md:hidden"> ${this.mobileSheet( 'Menu', html` <uip-button slot="trigger" variant="ghost" size="icon" class="[&::part(base)]:rounded-full" aria-label="Open navigation menu" > ${icon(Menu, 'menu', 'size-5')} </uip-button> <nav class="mt-6 grid gap-1" aria-label="Mobile"> ${pillLinks.map( (link) => html`<a href=${link.href} class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors" @click=${() => { this.sheetOpen = false }} > ${link.label} </a>`, )} </nav> `, 'top', )} </div> </header> </div> ` } private renderMega() { return html` <header data-slot="header-mega-menu" class="border-border bg-background/90 border-b backdrop-blur"> <div class="mx-auto flex h-16 max-w-6xl items-center gap-6 px-6"> <a href="#top" class="flex shrink-0 items-center gap-2 font-semibold tracking-tight"> ${icon(Layers, 'layers', 'text-primary size-5')} Northwind </a> <div class="hidden lg:flex"> <uip-navigation-menu> ${megaMenus.map( (menu) => html` <uip-navigation-menu-item> <span slot="trigger">${menu.label}</span> <div slot="content" class="grid w-[46rem] grid-cols-[1fr_1fr_16rem] gap-6 p-6"> ${menu.groups.map( (group) => html` <div> <p class="text-muted-foreground font-mono text-xs tracking-[0.14em] uppercase"> ${group.heading} </p> <ul class="mt-3 space-y-1"> ${group.items.map( (item) => html` <li> <a href="#" class="hover:bg-muted focus-visible:ring-ring flex gap-3 rounded-md p-2 transition-colors focus-visible:ring-2 focus-visible:outline-none" > ${icon(item.icon, item.iconName, 'text-muted-foreground mt-0.5 size-4 shrink-0')} <span class="min-w-0"> <span class="block text-sm font-medium">${item.title}</span> <span class="text-muted-foreground block text-xs leading-snug" >${item.body}</span > </span> </a> </li> `, )} </ul> </div> `, )} <div class="bg-muted/50 flex flex-col rounded-lg p-4"> <p class="text-sm font-semibold">${menu.highlight.title}</p> <p class="text-muted-foreground mt-2 text-xs leading-relaxed">${menu.highlight.body}</p> <uip-button variant="link" class="mt-auto h-auto justify-start p-0 pt-4 text-xs"> ${menu.highlight.cta} ${icon(ArrowRight, 'arrow-right', 'ml-1 size-3')} </uip-button> </div> </div> </uip-navigation-menu-item> `, )} ${megaPlainLinks.map( (link) => html` <uip-navigation-menu-item> <uip-navigation-menu-link href=${link.href} trigger-style>${link.label}</uip-navigation-menu-link> </uip-navigation-menu-item> `, )} </uip-navigation-menu> </div> <div class="ml-auto flex items-center gap-2"> <span class="hidden sm:contents"> <uip-button variant="ghost" size="sm">Sign in</uip-button> </span> <span class="hidden sm:contents"> <uip-separator orientation="vertical" class="[&::part(base)]:h-5!"></uip-separator> </span> <uip-button size="sm">Book a demo</uip-button> </div> <div class="ml-auto lg:hidden"> ${this.mobileSheet( 'Menu', html` <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open navigation menu"> ${icon(Menu, 'menu', 'size-5')} </uip-button> <nav class="mt-6 grid gap-5" aria-label="Mobile"> ${megaMenus.map( (menu) => html` <div> <p class="text-muted-foreground/70 text-xs font-medium tracking-wide uppercase"> ${menu.label} </p> <ul class="mt-2 grid gap-1"> ${menu.groups.map((group) => group.items.map( (item) => html` <li> <a href="#" class="hover:bg-muted block rounded-md px-3 py-2 text-sm font-medium transition-colors" @click=${() => { this.sheetOpen = false }} > ${item.title} </a> </li> `, ), )} </ul> </div> `, )} <uip-separator></uip-separator> ${megaPlainLinks.map( (link) => html`<a href=${link.href} class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors" @click=${() => { this.sheetOpen = false }} > ${link.label} </a>`, )} <uip-separator></uip-separator> <div class="grid gap-2"> <uip-button variant="outline">Sign in</uip-button> <uip-button>Book a demo</uip-button> </div> </nav> `, )} </div> </div> </header> ` } private renderSearch() { return html` <header data-slot="header-search-command" class="border-border bg-background/90 border-b backdrop-blur"> <div class="mx-auto flex h-16 max-w-6xl items-center gap-4 px-6"> <a href="#top" class="flex shrink-0 items-center gap-2 font-semibold tracking-tight"> ${icon(Compass, 'compass', 'text-primary size-5')} Halden </a> <nav class="hidden items-center gap-1 md:flex" aria-label="Primary"> ${searchLinks.map( (link) => html`<a href=${link.href} class="text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:ring-ring rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none" > ${link.label} </a>`, )} </nav> <button type="button" class="border-border bg-muted/40 text-muted-foreground hover:bg-muted focus-visible:ring-ring ml-auto flex h-9 w-full max-w-64 items-center gap-2 rounded-md border px-3 text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none" @click=${() => { this.searchOpen = true }} > ${icon(Search, 'search', 'size-4 shrink-0')} <span class="truncate">Search docs…</span> <span class="hidden sm:contents"> <uip-kbd class="ml-auto">⌘K</uip-kbd> </span> </button> <span class="hidden md:contents"> <uip-separator orientation="vertical" class="[&::part(base)]:h-5!"></uip-separator> </span> <span class="hidden sm:contents"> <uip-button size="sm" class="shrink-0">Start free</uip-button> </span> <div class="shrink-0 md:hidden"> ${this.mobileSheet( 'Menu', html` <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open navigation menu"> ${icon(Menu, 'menu', 'size-5')} </uip-button> <nav class="mt-6 grid gap-1" aria-label="Mobile"> ${searchLinks.map( (link) => html`<a href=${link.href} class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors" @click=${() => { this.sheetOpen = false }} > ${link.label} </a>`, )} </nav> <uip-separator class="[&::part(base)]:my-5"></uip-separator> <div class="grid gap-2"> <uip-button variant="outline">Sign in</uip-button> <uip-button>Start free</uip-button> </div> `, )} </div> </div> <uip-command-dialog placeholder="Search documentation, guides, and stories…" .open=${this.searchOpen} @open-change=${(e: CustomEvent<{ open: boolean }>) => { this.searchOpen = e.detail.open }} > <uip-command-empty>No results.</uip-command-empty> ${searchGroups.map( (group) => html` <uip-command-group heading=${group.heading}> ${group.items.map( (item) => html` <uip-command-item value=${item.label}> ${icon(item.icon, item.iconName, 'text-muted-foreground mr-2 size-4')} ${item.label} </uip-command-item> `, )} </uip-command-group> `, )} </uip-command-dialog> </header> ` } private renderTransparent() { return html` <header data-slot="header-transparent-scroll" class=${cn( 'fixed inset-x-0 top-0 z-40 transition-colors duration-200', this.scrolled ? 'border-border bg-background/85 border-b backdrop-blur' : 'border-b border-transparent', )} > <div class="mx-auto flex h-16 max-w-6xl items-center gap-6 px-6"> <a href="#top" class="flex items-center gap-2 font-semibold tracking-tight"> ${icon(Layers, 'layers', 'text-primary size-5')} Northwind </a> <nav class="hidden items-center gap-1 md:flex" aria-label="Primary"> ${transparentLinks.map( (link) => html`<a href=${link.href} class="text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:ring-ring rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none" > ${link.label} </a>`, )} </nav> <div class="ml-auto hidden items-center gap-2 md:flex"> <uip-button variant="ghost" size="sm">Sign in</uip-button> <uip-button size="sm">Start free</uip-button> </div> <div class="ml-auto md:hidden"> ${this.mobileSheet( 'Menu', html` <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open navigation menu"> ${icon(Menu, 'menu', 'size-5')} </uip-button> <nav class="mt-6 grid gap-1" aria-label="Mobile"> ${transparentLinks.map( (link) => html`<a href=${link.href} class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors" @click=${() => { this.sheetOpen = false }} > ${link.label} </a>`, )} </nav> <uip-separator class="[&::part(base)]:my-5"></uip-separator> <div class="grid gap-2"> <uip-button variant="outline">Sign in</uip-button> <uip-button>Start free</uip-button> </div> `, )} </div> </div> </header> ` } } customElements.get('uip-header') || customElements.define('uip-header', UipHeader) declare global { interface HTMLElementTagNameMap { 'uip-header': UipHeader } }
Raw manifest:https://uipkge.dev/r/lit/header.json