Header — Mega Menu
blockmarketingNavbar whose product and resources items open a full-width mega panel of grouped, described links plus a highlighted feature card, keyboard navigable and closing on Escape.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/header-mega-menu.json$npx shadcn@latest add https://uipkge.dev/r/lit/header-mega-menu.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/header-mega-menu.json$bunx shadcn@latest add https://uipkge.dev/r/lit/header-mega-menu.jsonManifest:
https://uipkge.dev/r/lit/header-mega-menu.jsonInstalls to:src/uip/blocks/header-mega-menu/Variants
Loading interactive previews…
Loading interactive previews…
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-mega-menu': UipHeaderMegaMenu
}
}Files installed (1)
src/uip/blocks/header-mega-menu/header-mega-menu.ts11.5 kB
import { LitElement, css, html } from 'lit' import { ArrowRight, BarChart3, BookOpen, Boxes, Layers, LifeBuoy, Lock, Menu, Workflow, type IconNode, } from 'lucide' 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/button/button' import '../../components/navigation-menu/navigation-menu' import '../../components/separator/separator' import '../../components/sheet/sheet' 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 menus: 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 plainLinks = [ { label: 'Pricing', href: '#pricing' }, { label: 'Customers', href: '#customers' }, ] /** * <uip-header-mega-menu> — the registry HeaderMegaMenu block as a web component. * * Class strings are React's HeaderMegaMenu verbatim. React state (useState) * becomes @state fields; the desktop dropdowns become <uip-navigation-menu> * (trigger/content slots, panel content stays light DOM) and the mobile menu * becomes <uip-sheet> (open + open-change). Arrow-key movement and Escape * come from the navigation-menu primitive. */ export class UipHeaderMegaMenu 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 = { open: { state: true }, } private open = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'header-mega-menu') } render() { 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> <!-- Plain wrapper carries the responsive class: display utilities can't go on the uip-navigation-menu host (its :host display wins). --> <div class="hidden lg:flex"> <uip-navigation-menu> ${menus.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> `, )} ${plainLinks.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"> <!-- Plain spans carry the responsive classes: display utilities can't go on uip-* hosts (their :host display wins). sm:contents keeps the primitives direct flex items with the row's gap. --> <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"> <uip-sheet side="right" heading="Menu" .open=${this.open} @open-change=${(e: CustomEvent<{ open: boolean }>) => { this.open = e.detail.open }} > <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"> ${menus.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.open = false }} > ${item.title} </a> </li> `, ), )} </ul> </div> `, )} <uip-separator></uip-separator> ${plainLinks.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.open = 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> </uip-sheet> </div> </div> </header> ` } } customElements.get('uip-header-mega-menu') || customElements.define('uip-header-mega-menu', UipHeaderMegaMenu) declare global { interface HTMLElementTagNameMap { 'uip-header-mega-menu': UipHeaderMegaMenu } }
Raw manifest:https://uipkge.dev/r/lit/header-mega-menu.json