Skip to main content

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>