Navigation
Sidebar code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/sidebar.blade.php
@props([
'brand' => null,
])
@php
/*
| Vertical app navigation rail. Compose <x-ui.sidebar.item> rows in the
| default slot; use the `header` slot for a logo and `footer` for a user
| block. Designed for dashboard / app-shell layouts.
|
| Usage:
| <x-ui.sidebar>
| <x-slot:header><x-ui.brand /></x-slot:header>
| <x-ui.sidebar.item icon="house" href="/dashboard" :active="true">Dashboard</x-ui.sidebar.item>
| <x-ui.sidebar.item icon="users" href="/team">Team</x-ui.sidebar.item>
| </x-ui.sidebar>
*/
@endphp
<aside {{ $attributes->merge(['class' => 'flex h-full w-64 flex-col border-e border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900']) }}>
@isset($header)
<div class="flex h-16 shrink-0 items-center border-b border-gray-200 dark:border-gray-800 px-5">
{{ $header }}
</div>
@endisset
<nav class="flex-1 space-y-1 overflow-y-auto p-3">
{{ $slot }}
</nav>
@isset($footer)
<div class="shrink-0 border-t border-gray-200 dark:border-gray-800 p-3">
{{ $footer }}
</div>
@endisset
</aside>
resources/views/components/ui/sidebar/branch.blade.php
@props([
'label',
'href' => '#',
'active' => false,
])
@php
$state = $active
? 'bg-gray-100 font-medium text-gray-900 dark:bg-gray-800 dark:text-white'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800/60 dark:hover:text-white';
@endphp
<div
data-sidebar-branch="{{ $label }}"
{{ $attributes->merge(['class' => 'space-y-0.5']) }}
>
<a
href="{{ $href }}"
data-sidebar-label="{{ $label }}"
@if ($active) aria-current="page" @endif
class="block rounded-lg py-1.5 pe-3 ps-9 text-sm transition {{ $state }}"
>
{{ $label }}
</a>
<div class="ms-9 border-s border-gray-300/70 ps-1.5 dark:border-gray-700/70">
{{ $slot }}
</div>
</div>
resources/views/components/ui/sidebar/branch/item.blade.php
@props([
'href' => '#',
'active' => false,
])
@php
$state = $active
? 'bg-gray-100 font-medium text-gray-900 dark:bg-gray-800 dark:text-white'
: 'text-gray-500 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-500 dark:hover:bg-gray-800/60 dark:hover:text-white';
$filterLabel = trim(strip_tags((string) $slot));
@endphp
<a
href="{{ $href }}"
data-sidebar-depth="3"
data-sidebar-label="{{ $filterLabel }}"
@if ($active) aria-current="page" @endif
{{ $attributes->merge(['class' => "block rounded-lg py-1.5 pe-3 ps-3 text-[13px] transition {$state}"]) }}
>
{{ $slot }}
</a>
resources/views/components/ui/sidebar/group.blade.php
@props([
'label',
'icon' => null,
'expanded' => false,
'active' => false,
'nestedSurface' => false,
])
@php
/*
| Expandable navigation section inside <x-ui.sidebar>. Pass `:expanded="true"`
| to open by default — useful when a nested item matches the current route.
|
| Usage:
| <x-ui.sidebar.group label="Settings" icon="gear" :expanded="request()->routeIs('settings.*')">
| <x-ui.sidebar.subitem href="/settings/profile">Profile</x-ui.sidebar.subitem>
| <x-ui.sidebar.subitem href="/settings/billing">Billing</x-ui.sidebar.subitem>
| </x-ui.sidebar.group>
*/
$state = $active
? 'text-gray-900 dark:text-white'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/60 hover:text-gray-900 dark:hover:text-white';
@endphp
<div
x-data="{
expanded: @js((bool) $expanded),
initialExpanded: @js((bool) $expanded),
}"
data-sidebar-group="{{ $label }}"
@sidebar-group-filter="
if ($event.detail.reset) {
expanded = initialExpanded;
} else if ($event.detail.expand) {
expanded = true;
}
"
>
<button
type="button"
@click="expanded = ! expanded"
:aria-expanded="expanded"
{{ $attributes->merge(['class' => "group flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition {$state}"]) }}
>
@if ($icon)
<x-ui.icon
:name="$icon"
:weight="$active ? 'fill' : 'regular'"
size="lg"
class="shrink-0 {{ $active ? 'text-brand-600 dark:text-brand-400' : 'text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-300' }}"
/>
@endif
<span class="flex-1 text-left">{{ $label }}</span>
<x-ui.icon
name="caret-down"
weight="bold"
size="base"
::class="expanded ? 'rotate-180' : ''"
class="shrink-0 text-gray-400 transition-transform duration-200 dark:text-gray-500"
/>
</button>
<div
x-show="expanded"
x-collapse
x-cloak
data-sidebar-group-items
@if ($nestedSurface) data-sidebar-nesting="surface" @endif
@class([
'mt-0.5 space-y-0.5',
'ms-4 border-s border-gray-300/70 ps-0.5 dark:border-gray-700/70' => $nestedSurface,
])
>
{{ $slot }}
</div>
</div>
resources/views/components/ui/sidebar/item.blade.php
@props([
'href' => '#',
'icon' => null,
'active' => false,
'badge' => null,
])
@php
/*
| A navigation row inside <x-ui.sidebar>. Set `active` for the current route
| and optionally pass a `badge` count.
|
| Usage:
| <x-ui.sidebar.item icon="house" href="/dashboard" :active="request()->routeIs('dashboard')">Dashboard</x-ui.sidebar.item>
| <x-ui.sidebar.item icon="envelope" href="/inbox" badge="12">Inbox</x-ui.sidebar.item>
*/
$state = $active
? 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/60 hover:text-gray-900 dark:hover:text-white';
$filterLabel = trim(strip_tags((string) $slot));
@endphp
<a href="{{ $href }}"
data-sidebar-label="{{ $filterLabel }}"
@if ($active) aria-current="page" @endif
{{ $attributes->merge(['class' => "group flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition {$state}"]) }}>
@if ($icon)
<x-ui.icon :name="$icon" :weight="$active ? 'fill' : 'regular'" size="lg" class="shrink-0 {{ $active ? 'text-brand-600 dark:text-brand-400' : 'text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-300' }}" />
@endif
<span class="flex-1">{{ $slot }}</span>
@if ($badge !== null)
<span class="inline-flex items-center justify-center rounded-full bg-gray-200 dark:bg-gray-700 px-2 py-0.5 text-xs font-semibold text-gray-700 dark:text-gray-200">{{ $badge }}</span>
@endif
</a>
resources/views/components/ui/sidebar/menu.blade.php
@props([
'placeholder' => 'Filter menu...',
])
@php
/*
| Filterable sidebar navigation wrapper. Place <x-ui.sidebar.item>,
| <x-ui.sidebar.group>, and <x-ui.sidebar.subitem> children in the default
| slot — they are filtered client-side by label as the user types.
|
| Usage:
| <x-ui.sidebar.menu>
| <x-ui.sidebar.item icon="house" href="/dashboard">Dashboard</x-ui.sidebar.item>
| <x-ui.sidebar.group label="Settings" icon="gear">
| <x-ui.sidebar.subitem href="/settings/profile">Profile</x-ui.sidebar.subitem>
| </x-ui.sidebar.group>
| </x-ui.sidebar.menu>
*/
$inputId = 'sidebar-filter-'.\Illuminate\Support\Str::random(8);
@endphp
<div
x-data="{
query: '',
matches(label) {
const q = this.query.toLowerCase().trim();
return q === '' || label.toLowerCase().includes(q);
},
applyFilter() {
const q = this.query.toLowerCase().trim();
const nav = this.$refs.nav;
nav.querySelectorAll(':scope > [data-sidebar-label]').forEach((el) => {
el.hidden = ! this.matches(el.dataset.sidebarLabel ?? '');
});
nav.querySelectorAll(':scope > [data-sidebar-group]').forEach((group) => {
const groupLabel = group.dataset.sidebarGroup ?? '';
const groupMatches = this.matches(groupLabel);
const items = group.querySelectorAll('[data-sidebar-label]');
let anyVisible = false;
items.forEach((item) => {
const visible = groupMatches || this.matches(item.dataset.sidebarLabel ?? '');
item.hidden = ! visible;
if (visible) {
anyVisible = true;
}
});
group.hidden = q !== '' && ! groupMatches && ! anyVisible;
group.dispatchEvent(new CustomEvent('sidebar-group-filter', {
detail: q === ''
? { reset: true }
: { expand: groupMatches || anyVisible },
}));
});
const hasVisible = q === ''
|| [...nav.querySelectorAll('[data-sidebar-label]')].some((el) => ! el.hidden)
|| [...nav.querySelectorAll('[data-sidebar-group]')].some((group) => ! group.hidden);
this.$refs.emptyState.hidden = hasVisible;
},
}"
x-init="applyFilter()"
x-effect="applyFilter()"
{{ $attributes->merge(['class' => 'space-y-2']) }}
>
<label class="sr-only" for="{{ $inputId }}">{{ $placeholder }}</label>
<x-ui.input
id="{{ $inputId }}"
type="search"
size="sm"
icon="magnifying-glass"
:placeholder="$placeholder"
x-model="query"
autocomplete="off"
/>
<div x-ref="nav" class="space-y-1">
{{ $slot }}
</div>
<p x-ref="emptyState" hidden class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
No matching items.
</p>
</div>
resources/views/components/ui/sidebar/subitem.blade.php
@props([
'href' => '#',
'active' => false,
])
@php
/*
| A nested link inside <x-ui.sidebar.group>.
|
| Usage:
| <x-ui.sidebar.subitem href="/settings/profile" :active="request()->routeIs('settings.profile')">
| Profile
| </x-ui.sidebar.subitem>
*/
$state = $active
? 'bg-gray-100 font-medium text-gray-900 dark:bg-gray-800 dark:text-white'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800/60 dark:hover:text-white';
$filterLabel = trim(strip_tags((string) $slot));
@endphp
<a
href="{{ $href }}"
data-sidebar-label="{{ $filterLabel }}"
@if ($active) aria-current="page" @endif
{{ $attributes->merge(['class' => "block rounded-lg py-1.5 pe-3 ps-9 text-sm transition {$state}"]) }}
>
{{ $slot }}
</a>