Header
Site headers — a navbar (bar or floating pill shape), dropdown nav, a mega-menu and a product-site header with grouped panels.
Preview
@props([
'brand' => 'Acme',
'brandHref' => '#',
// Top-level destinations, in order. Two shapes:
// ['label' => 'Docs', 'href' => '#', 'active' => false] → plain link
// ['label' => 'Browse', 'columns' => 2, 'items' => [...entries]] → panel
// A panel entry is ['title', 'description', 'href', 'icon', 'meta', 'badge', 'active'];
// `icon` is a key of $iconPaths below, `meta` a short trailing figure (a count),
// `badge` a short label ("New"). A panel is active when any entry is.
'nav' => null,
'cta' => 'Get started',
'ctaHref' => '#',
// Optional end-side repository link; null hides it.
'githubHref' => '#',
'githubLabel' => 'GitHub',
// The default search affordance is a static trigger; replace the `search`
// slot with a real command palette or combobox.
'searchLabel' => 'Search',
'searchShortcut' => '⌘K',
'navLabel' => 'Main',
'menuLabel' => 'Menu',
// id of the mobile <nav>, referenced by the hamburger's aria-controls.
'menuId' => 'site-header-menu',
// Inner row container; pass the app's own container classes so the brand
// lines up with page content.
'container' => 'mx-auto max-w-7xl px-4 sm:px-6 lg:px-8',
])
@php
$nav ??= [
['label' => 'Docs', 'href' => '#', 'active' => true],
[
'label' => 'Browse',
'columns' => 2,
'items' => [
['title' => 'Components', 'description' => 'Accessible primitives you copy into your app.', 'href' => '#', 'icon' => 'grid', 'meta' => '180'],
['title' => 'Blocks', 'description' => 'Composed sections ready to drop into a page.', 'href' => '#', 'icon' => 'layers', 'meta' => '420'],
['title' => 'Pages', 'description' => 'Full screens assembled from blocks.', 'href' => '#', 'icon' => 'file', 'meta' => '60'],
['title' => 'Bundles', 'description' => 'Dependency sets that install together.', 'href' => '#', 'icon' => 'package', 'badge' => 'New'],
],
],
[
'label' => 'Tools',
'items' => [
['title' => 'Builder', 'description' => 'Compose blocks into a page and export it.', 'href' => '#', 'icon' => 'layout'],
['title' => 'Themes', 'description' => 'Generate a token preset for your brand.', 'href' => '#', 'icon' => 'palette'],
],
],
['label' => 'Changelog', 'href' => '#'],
];
$iconPaths = [
'grid' => '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>',
'layers' => '<rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/>',
'file' => '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/>',
'package' => '<path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
'layout' => '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/>',
'palette' => '<circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 1 0 18c-1.5 0-2-.8-2-1.8 0-.9.4-1.2.4-2s-.6-1.2-1.7-1.2H7a4 4 0 0 1-4-4"/>',
'book' => '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z"/>',
'sparkles' => '<path d="M12 3l1.9 4.6 4.6 1.9-4.6 1.9L12 16l-1.9-4.6L5.5 9.5l4.6-1.9z"/><path d="M19 14l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8z"/>',
'bolt' => '<path d="M13 2 3 14h7l-1 8 10-12h-7l1-8z"/>',
'rocket' => '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/>',
'shield' => '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
'users' => '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
'lifebuoy' => '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/><path d="m4.9 4.9 4.2 4.2M14.9 14.9l4.2 4.2M19.1 4.9l-4.2 4.2M9.1 14.9l-4.2 4.2"/>',
'chart' => '<path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/>',
'code' => '<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>',
];
$isPanel = static fn (array $item): bool => ! empty($item['items']);
$panelActive = static fn (array $item): bool => (bool) ($item['active'] ?? collect($item['items'] ?? [])->contains(fn ($entry) => ! empty($entry['active'])));
@endphp
<header {{ $attributes->merge(['class' => 'sticky top-0 z-sticky w-full border-b border-border bg-background/85 text-foreground backdrop-blur-md']) }}>
<div class="{{ $container }} flex h-14 items-center gap-4 lg:gap-6">
{{-- Brand, pinned to the start --}}
@if (isset($logo) && $logo->isNotEmpty())
{{ $logo }}
@else
<x-ui.brand :href="$brandHref" :label="$brand" size="sm" class="shrink-0" />
@endif
{{-- Primary nav: plain links and hover/focus panels --}}
<div class="hidden min-w-0 lg:block">
<x-ui.navigation-menu aria-label="{{ __($navLabel) }}">
<x-ui.navigation-menu.list>
@foreach ($nav as $item)
<x-ui.navigation-menu.item>
@if ($isPanel($item))
<x-ui.navigation-menu.trigger :active="$panelActive($item)">{{ __($item['label']) }}</x-ui.navigation-menu.trigger>
@php $wide = ($item['columns'] ?? 1) > 1; @endphp
<x-ui.navigation-menu.content :class="$wide ? 'w-[38rem] p-2' : 'w-80 p-2'">
<ul @class(['grid gap-2', 'sm:grid-cols-2' => $wide])>
@foreach ($item['items'] as $entry)
<li>
<x-ui.navigation-menu.link :href="$entry['href'] ?? '#'" :active="! empty($entry['active'])" class="group">
<span class="flex items-start gap-4">
<span class="grid size-9 shrink-0 place-items-center rounded-md border border-border bg-muted text-muted-foreground transition-colors group-hover:text-foreground motion-reduce:transition-none">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{!! $iconPaths[$entry['icon'] ?? ''] ?? $iconPaths['bolt'] !!}</svg>
</span>
<span class="min-w-0 flex-1">
<span class="flex items-center gap-2">
<span class="truncate text-sm font-medium text-foreground">{{ __($entry['title']) }}</span>
@if (! empty($entry['badge']))
<x-ui.badge variant="secondary">{{ __($entry['badge']) }}</x-ui.badge>
@endif
@if (! empty($entry['meta']))
<span class="ms-auto shrink-0 text-xs tabular-nums text-muted-foreground">{{ $entry['meta'] }}</span>
@endif
</span>
@if (! empty($entry['description']))
<span class="mt-0.5 line-clamp-2 block text-xs text-muted-foreground">{{ __($entry['description']) }}</span>
@endif
</span>
</span>
</x-ui.navigation-menu.link>
</li>
@endforeach
</ul>
</x-ui.navigation-menu.content>
@else
<x-ui.navigation-menu.link :href="$item['href'] ?? '#'" :active="! empty($item['active'])" class="h-control-h-md px-control-x-md py-0 font-medium">
<span class="flex h-full items-center whitespace-nowrap">{{ __($item['label']) }}</span>
</x-ui.navigation-menu.link>
@endif
</x-ui.navigation-menu.item>
@endforeach
</x-ui.navigation-menu.list>
</x-ui.navigation-menu>
</div>
{{-- Search + utility actions + CTA + mobile menu, pinned to the end --}}
<div class="ms-auto flex min-w-0 flex-1 items-center justify-end gap-2 lg:flex-none">
@if (isset($search) && $search->isNotEmpty())
{{ $search }}
@else
<span class="hidden sm:block">
<x-ui.button variant="outline" size="sm" type="button" class="w-56 justify-start text-muted-foreground [&>[data-slot=button-label]]:flex-1 [&>[data-slot=button-label]]:text-start">
<x-slot:icon>
<svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
</x-slot:icon>
{{ __($searchLabel) }}
@if ($searchShortcut)
<x-slot:iconTrailing>
<x-ui.kbd>{{ $searchShortcut }}</x-ui.kbd>
</x-slot:iconTrailing>
@endif
</x-ui.button>
</span>
@endif
@if ($githubHref)
<span class="hidden sm:block">
<x-ui.button :href="$githubHref" variant="outline" size="sm" :icon-label="__($githubLabel)" rel="noopener" class="text-muted-foreground">
<svg class="size-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2.17c-3.2.7-3.87-1.37-3.87-1.37-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.19 1.76 1.19 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.17 1.18a11 11 0 0 1 5.77 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.8 1.19 1.83 1.19 3.09 0 4.42-2.7 5.39-5.26 5.68.41.36.78 1.06.78 2.14v3.17c0 .31.21.67.8.56A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z"/></svg>
</x-ui.button>
</span>
@endif
{{ $actions ?? '' }}
<span class="hidden lg:block">
<x-ui.button :href="$ctaHref" size="sm" class="whitespace-nowrap">{{ __($cta) }}</x-ui.button>
</span>
{{-- Mobile menu: an end-side sheet --}}
<x-ui.sheet>
<x-ui.button
variant="outline"
size="sm"
:icon-label="__($menuLabel)"
class="text-muted-foreground lg:hidden"
x-on:click="show()"
aria-haspopup="dialog"
x-bind:aria-expanded="open"
aria-controls="{{ $menuId }}"
x-ref="trigger"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</x-ui.button>
<x-ui.sheet.content side="end" class="flex flex-col !p-0">
<div class="flex shrink-0 items-center justify-between gap-4 border-b border-border px-4 py-2">
<x-ui.sheet.title class="flex min-w-0 items-center text-sm">
@if (isset($logo) && $logo->isNotEmpty())
{{ $logo }}
@else
<x-ui.brand as="span" :label="$brand" size="sm" />
@endif
</x-ui.sheet.title>
<x-ui.button variant="ghost" size="sm" :icon-label="__('Close menu')" x-on:click="hide()" class="text-muted-foreground">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>
</x-ui.button>
</div>
<nav id="{{ $menuId }}" aria-label="{{ __('Mobile') }}" class="flex-1 overflow-y-auto p-4">
<ul class="grid gap-2">
@foreach ($nav as $item)
@if ($isPanel($item))
<li class="grid gap-2 pt-2">
<p class="px-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ __($item['label']) }}</p>
<ul class="grid gap-2">
@foreach ($item['items'] as $entry)
<li>
<a
href="{{ $entry['href'] ?? '#' }}"
data-active="{{ ! empty($entry['active']) ? 'true' : 'false' }}"
@if (! empty($entry['active'])) aria-current="page" @endif
class="flex h-control-h-md items-center gap-4 rounded-md px-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring data-[active=true]:bg-accent/50 motion-reduce:transition-none"
>
<span class="grid size-8 shrink-0 place-items-center rounded-md border border-border bg-muted text-muted-foreground">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{!! $iconPaths[$entry['icon'] ?? ''] ?? $iconPaths['bolt'] !!}</svg>
</span>
<span class="min-w-0 flex-1 truncate">{{ __($entry['title']) }}</span>
@if (! empty($entry['badge']))
<x-ui.badge variant="secondary">{{ __($entry['badge']) }}</x-ui.badge>
@endif
@if (! empty($entry['meta']))
<span class="shrink-0 text-xs tabular-nums text-muted-foreground">{{ $entry['meta'] }}</span>
@endif
</a>
</li>
@endforeach
</ul>
</li>
@else
<li>
<a
href="{{ $item['href'] ?? '#' }}"
data-active="{{ ! empty($item['active']) ? 'true' : 'false' }}"
@if (! empty($item['active'])) aria-current="page" @endif
class="flex h-control-h-md items-center rounded-md px-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring data-[active=true]:bg-accent/50 motion-reduce:transition-none"
>{{ __($item['label']) }}</a>
</li>
@endif
@endforeach
</ul>
</nav>
<div class="grid shrink-0 gap-2 border-t border-border p-4">
{{ $mobile ?? '' }}
@if ($githubHref)
<x-ui.button :href="$githubHref" variant="outline" rel="noopener" class="w-full sm:hidden">{{ __($githubLabel) }}</x-ui.button>
@endif
<x-ui.button :href="$ctaHref" class="w-full">{{ __($cta) }}</x-ui.button>
</div>
</x-ui.sheet.content>
</x-ui.sheet>
</div>
</div>
</header>
Installation
php artisan ui:add blocks/header-product
Registry contract
Install confidence
php artisan ui:add blocks/header-product
writes only the generated targets below. The CLI validates each file hash before writing and
prompts before replacing local changes unless --force is used.
- Version
- 1.0.2
- License
- open
- Stability
- stable
- Contract
- v4
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/header-product.blade.php |
Registry dependencies
Package dependencies
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.
# Brok UI block: Header Product (`header-product`)
A product-site header — brand, a navigation menu with plain links and hover/focus panels of icon-tiled entries (description, count, badge, active state), a search trigger slot, utility actions, a primary CTA, and an end-side sheet for the mobile menu.
Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.
## Install
```bash
php artisan ui:add blocks/header-product
```
## Render it
```blade
<x-blocks.header-product />
```
## Props
- `brand` (string, default `Acme`) — Brand name; its first letter fills the default mark.
- `brand-href` (string, default `#`) — Brand link target.
- `nav` (array|null, default `null`) — Top-level items. A link is ['label','href','active']; a panel is ['label','columns','items'] whose entries are ['title','description','href','icon','meta','badge','active']. A panel is active when any entry is.
- `cta` (string, default `Get started`) — Primary action label, shown on desktop and in the sheet footer.
- `cta-href` (string, default `#`) — Primary action target.
- `github-href` (string|null, default `#`) — Repository icon link (end side); null hides it.
- `github-label` (string, default `GitHub`) — Accessible name of the repository link.
- `search-label` (string, default `Search`) — Label of the default static search trigger.
- `search-shortcut` (string|null, default `⌘K`) — Shortcut hint rendered as a kbd in the default search trigger; null hides it.
- `nav-label` (string, default `Main`) — aria-label of the desktop navigation landmark.
- `menu-label` (string, default `Menu`) — Accessible name of the hamburger button.
- `menu-id` (string, default `site-header-menu`) — id of the mobile nav, referenced by the hamburger's aria-controls.
- `container` (string, default `mx-auto max-w-7xl px-4 sm:px-6 lg:px-8`) — Classes of the inner row container; pass the app's own container so the brand aligns with page content.
## Use when
- Use for a documentation or product site whose destinations group into a few panels (Browse, Tools) plus a couple of plain links.
- Use when the header must host app-owned controls (a search palette, a theme toggle) through slots instead of hard-coded markup.
- Use when the current destination must read as active in both the desktop menu and the mobile sheet.
## Avoid when
- Choose header-navbar for a flat list of four or fewer links with no panels.
- Choose header-mega-menu when the panel needs a featured promo card rather than a list of destinations.
- Choose app-header-* for an authenticated application shell with user menus and breadcrumbs.
## Rules
- Render the installed block with `<x-blocks.header-product />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.
## Links
- Docs: https://brokui.dev/blocks/header-product
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/header-product.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Guidance
Use when
- Use for a documentation or product site whose destinations group into a few panels (Browse, Tools) plus a couple of plain links.
- Use when the header must host app-owned controls (a search palette, a theme toggle) through slots instead of hard-coded markup.
- Use when the current destination must read as active in both the desktop menu and the mobile sheet.
Avoid when
- Choose header-navbar for a flat list of four or fewer links with no panels.
- Choose header-mega-menu when the panel needs a featured promo card rather than a list of destinations.
- Choose app-header-* for an authenticated application shell with user menus and breadcrumbs.
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
- Anatomy
- Theming hooks
Usage
Render the block as a component, passing data where useful:
<x-blocks.header-product />
Built from primitives
This block composes installed <brok:*> primitives and semantic design
tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by
editing CSS variables; flip the preview to RTL to confirm it mirrors.
Source
The exact, editable file ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
@props([
'brand' => 'Acme',
'brandHref' => '#',
// Top-level destinations, in order. Two shapes:
// ['label' => 'Docs', 'href' => '#', 'active' => false] → plain link
// ['label' => 'Browse', 'columns' => 2, 'items' => [...entries]] → panel
// A panel entry is ['title', 'description', 'href', 'icon', 'meta', 'badge', 'active'];
// `icon` is a key of $iconPaths below, `meta` a short trailing figure (a count),
// `badge` a short label ("New"). A panel is active when any entry is.
'nav' => null,
'cta' => 'Get started',
'ctaHref' => '#',
// Optional end-side repository link; null hides it.
'githubHref' => '#',
'githubLabel' => 'GitHub',
// The default search affordance is a static trigger; replace the `search`
// slot with a real command palette or combobox.
'searchLabel' => 'Search',
'searchShortcut' => '⌘K',
'navLabel' => 'Main',
'menuLabel' => 'Menu',
// id of the mobile <nav>, referenced by the hamburger's aria-controls.
'menuId' => 'site-header-menu',
// Inner row container; pass the app's own container classes so the brand
// lines up with page content.
'container' => 'mx-auto max-w-7xl px-4 sm:px-6 lg:px-8',
])
@php
$nav ??= [
['label' => 'Docs', 'href' => '#', 'active' => true],
[
'label' => 'Browse',
'columns' => 2,
'items' => [
['title' => 'Components', 'description' => 'Accessible primitives you copy into your app.', 'href' => '#', 'icon' => 'grid', 'meta' => '180'],
['title' => 'Blocks', 'description' => 'Composed sections ready to drop into a page.', 'href' => '#', 'icon' => 'layers', 'meta' => '420'],
['title' => 'Pages', 'description' => 'Full screens assembled from blocks.', 'href' => '#', 'icon' => 'file', 'meta' => '60'],
['title' => 'Bundles', 'description' => 'Dependency sets that install together.', 'href' => '#', 'icon' => 'package', 'badge' => 'New'],
],
],
[
'label' => 'Tools',
'items' => [
['title' => 'Builder', 'description' => 'Compose blocks into a page and export it.', 'href' => '#', 'icon' => 'layout'],
['title' => 'Themes', 'description' => 'Generate a token preset for your brand.', 'href' => '#', 'icon' => 'palette'],
],
],
['label' => 'Changelog', 'href' => '#'],
];
$iconPaths = [
'grid' => '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>',
'layers' => '<rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/>',
'file' => '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/>',
'package' => '<path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
'layout' => '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/>',
'palette' => '<circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 1 0 18c-1.5 0-2-.8-2-1.8 0-.9.4-1.2.4-2s-.6-1.2-1.7-1.2H7a4 4 0 0 1-4-4"/>',
'book' => '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z"/>',
'sparkles' => '<path d="M12 3l1.9 4.6 4.6 1.9-4.6 1.9L12 16l-1.9-4.6L5.5 9.5l4.6-1.9z"/><path d="M19 14l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8z"/>',
'bolt' => '<path d="M13 2 3 14h7l-1 8 10-12h-7l1-8z"/>',
'rocket' => '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/>',
'shield' => '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
'users' => '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
'lifebuoy' => '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/><path d="m4.9 4.9 4.2 4.2M14.9 14.9l4.2 4.2M19.1 4.9l-4.2 4.2M9.1 14.9l-4.2 4.2"/>',
'chart' => '<path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/>',
'code' => '<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>',
];
$isPanel = static fn (array $item): bool => ! empty($item['items']);
$panelActive = static fn (array $item): bool => (bool) ($item['active'] ?? collect($item['items'] ?? [])->contains(fn ($entry) => ! empty($entry['active'])));
@endphp
<header {{ $attributes->merge(['class' => 'sticky top-0 z-sticky w-full border-b border-border bg-background/85 text-foreground backdrop-blur-md']) }}>
<div class="{{ $container }} flex h-14 items-center gap-4 lg:gap-6">
{{-- Brand, pinned to the start --}}
@if (isset($logo) && $logo->isNotEmpty())
{{ $logo }}
@else
<x-ui.brand :href="$brandHref" :label="$brand" size="sm" class="shrink-0" />
@endif
{{-- Primary nav: plain links and hover/focus panels --}}
<div class="hidden min-w-0 lg:block">
<x-ui.navigation-menu aria-label="{{ __($navLabel) }}">
<x-ui.navigation-menu.list>
@foreach ($nav as $item)
<x-ui.navigation-menu.item>
@if ($isPanel($item))
<x-ui.navigation-menu.trigger :active="$panelActive($item)">{{ __($item['label']) }}</x-ui.navigation-menu.trigger>
@php $wide = ($item['columns'] ?? 1) > 1; @endphp
<x-ui.navigation-menu.content :class="$wide ? 'w-[38rem] p-2' : 'w-80 p-2'">
<ul @class(['grid gap-2', 'sm:grid-cols-2' => $wide])>
@foreach ($item['items'] as $entry)
<li>
<x-ui.navigation-menu.link :href="$entry['href'] ?? '#'" :active="! empty($entry['active'])" class="group">
<span class="flex items-start gap-4">
<span class="grid size-9 shrink-0 place-items-center rounded-md border border-border bg-muted text-muted-foreground transition-colors group-hover:text-foreground motion-reduce:transition-none">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{!! $iconPaths[$entry['icon'] ?? ''] ?? $iconPaths['bolt'] !!}</svg>
</span>
<span class="min-w-0 flex-1">
<span class="flex items-center gap-2">
<span class="truncate text-sm font-medium text-foreground">{{ __($entry['title']) }}</span>
@if (! empty($entry['badge']))
<x-ui.badge variant="secondary">{{ __($entry['badge']) }}</x-ui.badge>
@endif
@if (! empty($entry['meta']))
<span class="ms-auto shrink-0 text-xs tabular-nums text-muted-foreground">{{ $entry['meta'] }}</span>
@endif
</span>
@if (! empty($entry['description']))
<span class="mt-0.5 line-clamp-2 block text-xs text-muted-foreground">{{ __($entry['description']) }}</span>
@endif
</span>
</span>
</x-ui.navigation-menu.link>
</li>
@endforeach
</ul>
</x-ui.navigation-menu.content>
@else
<x-ui.navigation-menu.link :href="$item['href'] ?? '#'" :active="! empty($item['active'])" class="h-control-h-md px-control-x-md py-0 font-medium">
<span class="flex h-full items-center whitespace-nowrap">{{ __($item['label']) }}</span>
</x-ui.navigation-menu.link>
@endif
</x-ui.navigation-menu.item>
@endforeach
</x-ui.navigation-menu.list>
</x-ui.navigation-menu>
</div>
{{-- Search + utility actions + CTA + mobile menu, pinned to the end --}}
<div class="ms-auto flex min-w-0 flex-1 items-center justify-end gap-2 lg:flex-none">
@if (isset($search) && $search->isNotEmpty())
{{ $search }}
@else
<span class="hidden sm:block">
<x-ui.button variant="outline" size="sm" type="button" class="w-56 justify-start text-muted-foreground [&>[data-slot=button-label]]:flex-1 [&>[data-slot=button-label]]:text-start">
<x-slot:icon>
<svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
</x-slot:icon>
{{ __($searchLabel) }}
@if ($searchShortcut)
<x-slot:iconTrailing>
<x-ui.kbd>{{ $searchShortcut }}</x-ui.kbd>
</x-slot:iconTrailing>
@endif
</x-ui.button>
</span>
@endif
@if ($githubHref)
<span class="hidden sm:block">
<x-ui.button :href="$githubHref" variant="outline" size="sm" :icon-label="__($githubLabel)" rel="noopener" class="text-muted-foreground">
<svg class="size-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2.17c-3.2.7-3.87-1.37-3.87-1.37-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.19 1.76 1.19 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.17 1.18a11 11 0 0 1 5.77 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.8 1.19 1.83 1.19 3.09 0 4.42-2.7 5.39-5.26 5.68.41.36.78 1.06.78 2.14v3.17c0 .31.21.67.8.56A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z"/></svg>
</x-ui.button>
</span>
@endif
{{ $actions ?? '' }}
<span class="hidden lg:block">
<x-ui.button :href="$ctaHref" size="sm" class="whitespace-nowrap">{{ __($cta) }}</x-ui.button>
</span>
{{-- Mobile menu: an end-side sheet --}}
<x-ui.sheet>
<x-ui.button
variant="outline"
size="sm"
:icon-label="__($menuLabel)"
class="text-muted-foreground lg:hidden"
x-on:click="show()"
aria-haspopup="dialog"
x-bind:aria-expanded="open"
aria-controls="{{ $menuId }}"
x-ref="trigger"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</x-ui.button>
<x-ui.sheet.content side="end" class="flex flex-col !p-0">
<div class="flex shrink-0 items-center justify-between gap-4 border-b border-border px-4 py-2">
<x-ui.sheet.title class="flex min-w-0 items-center text-sm">
@if (isset($logo) && $logo->isNotEmpty())
{{ $logo }}
@else
<x-ui.brand as="span" :label="$brand" size="sm" />
@endif
</x-ui.sheet.title>
<x-ui.button variant="ghost" size="sm" :icon-label="__('Close menu')" x-on:click="hide()" class="text-muted-foreground">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>
</x-ui.button>
</div>
<nav id="{{ $menuId }}" aria-label="{{ __('Mobile') }}" class="flex-1 overflow-y-auto p-4">
<ul class="grid gap-2">
@foreach ($nav as $item)
@if ($isPanel($item))
<li class="grid gap-2 pt-2">
<p class="px-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ __($item['label']) }}</p>
<ul class="grid gap-2">
@foreach ($item['items'] as $entry)
<li>
<a
href="{{ $entry['href'] ?? '#' }}"
data-active="{{ ! empty($entry['active']) ? 'true' : 'false' }}"
@if (! empty($entry['active'])) aria-current="page" @endif
class="flex h-control-h-md items-center gap-4 rounded-md px-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring data-[active=true]:bg-accent/50 motion-reduce:transition-none"
>
<span class="grid size-8 shrink-0 place-items-center rounded-md border border-border bg-muted text-muted-foreground">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{!! $iconPaths[$entry['icon'] ?? ''] ?? $iconPaths['bolt'] !!}</svg>
</span>
<span class="min-w-0 flex-1 truncate">{{ __($entry['title']) }}</span>
@if (! empty($entry['badge']))
<x-ui.badge variant="secondary">{{ __($entry['badge']) }}</x-ui.badge>
@endif
@if (! empty($entry['meta']))
<span class="shrink-0 text-xs tabular-nums text-muted-foreground">{{ $entry['meta'] }}</span>
@endif
</a>
</li>
@endforeach
</ul>
</li>
@else
<li>
<a
href="{{ $item['href'] ?? '#' }}"
data-active="{{ ! empty($item['active']) ? 'true' : 'false' }}"
@if (! empty($item['active'])) aria-current="page" @endif
class="flex h-control-h-md items-center rounded-md px-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring data-[active=true]:bg-accent/50 motion-reduce:transition-none"
>{{ __($item['label']) }}</a>
</li>
@endif
@endforeach
</ul>
</nav>
<div class="grid shrink-0 gap-2 border-t border-border p-4">
{{ $mobile ?? '' }}
@if ($githubHref)
<x-ui.button :href="$githubHref" variant="outline" rel="noopener" class="w-full sm:hidden">{{ __($githubLabel) }}</x-ui.button>
@endif
<x-ui.button :href="$ctaHref" class="w-full">{{ __($cta) }}</x-ui.button>
</div>
</x-ui.sheet.content>
</x-ui.sheet>
</div>
</div>
</header>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.2 - Status
- Stable
- License
-
open - Deprecation
- Not deprecated
- Contract
-
v4 - Foundation
-
≥ 1.0.0