Menu

Change language

GitHub repo

Header — Mega Menu

blockmarketing

Navbar 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

$npx shadcn@latest add https://uipkge.dev/r/lit/header-mega-menu.json
Manifest:https://uipkge.dev/r/lit/header-mega-menu.jsonInstalls to:src/uip/blocks/header-mega-menu/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

MegaItem
interface MegaItem {
  icon: IconNode
  iconName: string
  title: string
  body: string
}
MegaMenu
interface MegaMenu {
  label: string
  groups: { heading: string; items: MegaItem[] }[]
  highlight: { title: string; body: string; cta: string }
}
HTMLElementTagNameMap
interface 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