Skip to content
Brok UI

Loading…

No results

Sidebar

Open source Core

A collapsible application sidebar with persistent state, accessible navigation, and an optional draggable resize handle (clamped, localStorage-persisted width).

Version
v1.13.0
Stability
stable
License
MIT
Related
Navigation Menu
Table of Contents
Resizable

Preview

Dashboard

Fold the sidebar from the trigger; the rail keeps every destination as a centred icon with its name as a title.

Disabled
previews.components.sidebar.default.blade.php Blade
<div class="flex h-[32rem] w-full overflow-hidden rounded-lg border border-border" data-surface="admin">
    <x-ui.sidebar>
        <x-ui.sidebar.header>
            <x-ui.sidebar.trigger />
            <span x-show="expanded" class="truncate text-sm font-semibold text-foreground">{{ __('Acme Inc') }}</span>
        </x-ui.sidebar.header>

        <x-ui.sidebar.content>
            <x-ui.sidebar.group>
                <x-ui.sidebar.group-label>{{ __('Platform') }}</x-ui.sidebar.group-label>
                <x-ui.sidebar.menu>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :active="true" :label="__('Dashboard')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Inbox')" trailing="12"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Team')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Analytics')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>

            <x-ui.sidebar.group collapsible>
                <x-ui.sidebar.group-label collapsible>{{ __('Projects') }}</x-ui.sidebar.group-label>
                <x-ui.sidebar.menu :label="__('Projects')">
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Website relaunch')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></x-ui.sidebar.menu-button>
                        <x-ui.sidebar.menu-sub>
                            <x-ui.sidebar.menu-item>
                                <x-ui.sidebar.menu-button href="#" size="sm" :label="__('Design')" />
                            </x-ui.sidebar.menu-item>
                            <x-ui.sidebar.menu-item>
                                <x-ui.sidebar.menu-button href="#" size="sm" :label="__('Content')" />
                            </x-ui.sidebar.menu-item>
                        </x-ui.sidebar.menu-sub>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Mobile app')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>
        </x-ui.sidebar.content>

        <x-ui.sidebar.footer>
            <x-ui.sidebar.menu-button href="#" :label="__('Settings')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></x-ui.sidebar.menu-button>
        </x-ui.sidebar.footer>
    </x-ui.sidebar>

    <x-ui.sidebar.inset class="p-6">
        <h2 class="text-lg font-semibold text-foreground">{{ __('Dashboard') }}</h2>
        <p class="mt-2 text-sm text-muted-foreground">{{ __('Fold the sidebar from the trigger; the rail keeps every destination as a centred icon with its name as a title.') }}</p>
    </x-ui.sidebar.inset>
</div>

Installation

terminal
php artisan ui:add sidebar

Note

This component ships an Alpine behavior module at resources/js/ui/sidebar.js. Import it once from your bundle so it registers on alpine:init:

resources/js/ui/index.js JS
import './sidebar.js';

Registry contract

php artisan ui:add sidebar writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/sidebar.blade.php
  • blade resources/views/components/ui/sidebar/trigger.blade.php
  • blade resources/views/components/ui/sidebar/header.blade.php
  • blade resources/views/components/ui/sidebar/content.blade.php
  • blade resources/views/components/ui/sidebar/footer.blade.php
  • blade resources/views/components/ui/sidebar/group.blade.php
  • blade resources/views/components/ui/sidebar/group-label.blade.php
  • blade resources/views/components/ui/sidebar/menu.blade.php
  • blade resources/views/components/ui/sidebar/menu-item.blade.php
  • blade resources/views/components/ui/sidebar/menu-button.blade.php
  • blade resources/views/components/ui/sidebar/separator.blade.php
  • blade resources/views/components/ui/sidebar/inset.blade.php
  • js resources/js/ui/sidebar.js
  • blade resources/views/components/ui/sidebar/menu-sub.blade.php
  • blade resources/views/components/ui/sidebar/menu-count.blade.php
  • blade resources/views/components/ui/sidebar/mobile.blade.php
  • js public/vendor/brok/sidebar-group-boot.js
Registry dependencies
button separator tooltip sheet badge
Packages
composer: jml/brok:^0.2
npm: alpinejs

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

sidebar.md
# Brok UI: Sidebar (`sidebar`)

A collapsible application sidebar with persistent state, accessible navigation, and an optional draggable resize handle (clamped, localStorage-persisted width).

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add sidebar
```

## Usage

```blade
<div class="flex h-[32rem] w-full overflow-hidden rounded-lg border border-border" data-surface="admin">
    <x-ui.sidebar>
        <x-ui.sidebar.header>
            <x-ui.sidebar.trigger />
            <span x-show="expanded" class="truncate text-sm font-semibold text-foreground">{{ __('Acme Inc') }}</span>
        </x-ui.sidebar.header>

        <x-ui.sidebar.content>
            <x-ui.sidebar.group>
                <x-ui.sidebar.group-label>{{ __('Platform') }}</x-ui.sidebar.group-label>
                <x-ui.sidebar.menu>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :active="true" :label="__('Dashboard')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Inbox')" trailing="12"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Team')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Analytics')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>

            <x-ui.sidebar.group collapsible>
                <x-ui.sidebar.group-label collapsible>{{ __('Projects') }}</x-ui.sidebar.group-label>
                <x-ui.sidebar.menu :label="__('Projects')">
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Website relaunch')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></x-ui.sidebar.menu-button>
                        <x-ui.sidebar.menu-sub>
                            <x-ui.sidebar.menu-item>
                                <x-ui.sidebar.menu-button href="#" size="sm" :label="__('Design')" />
                            </x-ui.sidebar.menu-item>
                            <x-ui.sidebar.menu-item>
                                <x-ui.sidebar.menu-button href="#" size="sm" :label="__('Content')" />
                            </x-ui.sidebar.menu-item>
                        </x-ui.sidebar.menu-sub>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Mobile app')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>
        </x-ui.sidebar.content>

        <x-ui.sidebar.footer>
            <x-ui.sidebar.menu-button href="#" :label="__('Settings')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></x-ui.sidebar.menu-button>
        </x-ui.sidebar.footer>
    </x-ui.sidebar>

    <x-ui.sidebar.inset class="p-6">
        <h2 class="text-lg font-semibold text-foreground">{{ __('Dashboard') }}</h2>
        <p class="mt-2 text-sm text-muted-foreground">{{ __('Fold the sidebar from the trigger; the rail keeps every destination as a centred icon with its name as a title.') }}</p>
    </x-ui.sidebar.inset>
</div>
```

## Props

- `resizable` (bool, default `false`) — Adds a keyboard-operable separator on the inline-end edge that drags the expanded width between minWidth and maxWidth (Arrow keys step 8px, Shift 32px, Home/End to the limits, double-click resets).
- `minWidth` (int, default `200`) — Narrowest expanded width in px when resizable.
- `maxWidth` (int, default `480`) — Widest expanded width in px when resizable.
- `defaultWidth` (int, default `256`) — Expanded width in px before the visitor resizes, and the double-click reset target.
- `storageKey` (string|null, default `null`) — localStorage key that persists the resized width; null keeps the width for the session only. Reads and writes are guarded, so blocked storage never breaks the sidebar.
- `stateKey` (string, default `sidebar:state`) — localStorage key for the expanded/collapsed state. Give each shell its own key (for example brok:app-shell-06:sidebar) when one app renders several shells that should not follow each other.
- `variant` (sidebar|inset, default `sidebar`) — `sidebar` is a card-coloured rail with an inline-end border. `inset` puts the rail straight on the canvas colour with no border, for a shell whose content sits in a raised panel (pair it with `inset.variant`). Unknown values fall back to `sidebar`.
- `disabled` (false|true, default `false`) — Documented catalog control used by the preview workbench.
- `collapsible` (bool, default `false`) — Declared by @props in the registry Blade source.
- `open` (bool, default `true`) — Declared by @props in the registry Blade source.
- `bootSrc` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `nonce` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `label` (string, default `Primary`) — Declared by @props in the registry Blade source.
- `href` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `active` (bool, default `false`) — Declared by @props in the registry Blade source.
- `size` (string, default `md`) — Declared by @props in the registry Blade source.
- `trailing` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `count` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `countExpr` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `countLabel` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `soon` (bool, default `false`) — Declared by @props in the registry Blade source.
- `type` (string, default `button`) — Declared by @props in the registry Blade source.
- `shortcut` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `when` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `nav` (array, default `[]`) — Declared by @props in the registry Blade source.
- `bare` (bool, default `false`) — Declared by @props in the registry Blade source.
- `id` (mixed|null, default `null`) — Declared by @props in the registry Blade source.

## Use when

- Use to orient users and help them move across pages, sections, or commands.
- Building an application shell with persistent side navigation, grouped menu items, and a collapsible rail.
- Needing the sidebar's expanded/collapsed state and width to persist across visits via localStorage.

## Avoid when

- Do not hide primary wayfinding in novelty interactions or deep nested structures if straightforward navigation would be clearer.
- The navigation is a small, flat set of links; use navigation-menu or a plain nav instead of the full app-shell sidebar.
- The page is a marketing or docs page rather than an authenticated app shell; use table-of-contents or a simple nav.

## Anti-patterns

- Hiding primary wayfinding in novelty interactions

## Rules

- Use the `<brok:sidebar>` tag (or `<x-ui.sidebar>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/sidebar
- Registry JSON (files, props, contract): https://brokui.dev/r/open/sidebar.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

counts.blade.php Blade
{{-- Live counts and destinations that are not built yet. The Inbox count
     follows an Alpine value (a Livewire property works the same way) and
     hides at zero; Reports is marked Soon and is not a link. --}}
<div class="flex h-[28rem] w-full overflow-hidden rounded-lg border border-border" x-data="{ unread: 3 }">
    <x-ui.sidebar>
        <x-ui.sidebar.header>
            <x-ui.sidebar.trigger />
            <span x-show="expanded" class="truncate text-sm font-semibold text-foreground">{{ __('Workspace') }}</span>
        </x-ui.sidebar.header>

        <x-ui.sidebar.content>
            <x-ui.sidebar.group>
                <x-ui.sidebar.menu>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :active="true" :label="__('Inbox')" :count="3" count-expr="unread" :count-label="__('unread')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Library')" :count="128"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Archive')" :count="0"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Reports')" soon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>
        </x-ui.sidebar.content>
    </x-ui.sidebar>

    <x-ui.sidebar.inset class="p-6">
        <div class="flex flex-wrap gap-2">
            <x-ui.button size="sm" variant="outline" x-on:click="unread++">{{ __('New item') }}</x-ui.button>
            <x-ui.button size="sm" variant="outline" x-on:click="unread = 0">{{ __('Mark all read') }}</x-ui.button>
        </div>
    </x-ui.sidebar.inset>
</div>
inset.blade.php Blade
<div class="flex h-[32rem] w-full overflow-hidden rounded-lg border border-border bg-canvas" data-surface="admin">
    <x-ui.sidebar variant="inset">
        <x-ui.sidebar.header>
            <x-ui.sidebar.trigger />
            <span x-show="expanded" class="truncate text-sm font-semibold text-foreground">{{ __('Acme Inc') }}</span>
        </x-ui.sidebar.header>

        <x-ui.sidebar.content>
            <x-ui.sidebar.group>
                <x-ui.sidebar.group-label>{{ __('Platform') }}</x-ui.sidebar.group-label>
                <x-ui.sidebar.menu>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :active="true" :label="__('Dashboard')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Inbox')" trailing="12"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Team')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Analytics')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>

            <x-ui.sidebar.group collapsible>
                <x-ui.sidebar.group-label collapsible>{{ __('Projects') }}</x-ui.sidebar.group-label>
                <x-ui.sidebar.menu :label="__('Projects')">
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Website relaunch')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></x-ui.sidebar.menu-button>
                        <x-ui.sidebar.menu-sub>
                            <x-ui.sidebar.menu-item>
                                <x-ui.sidebar.menu-button href="#" size="sm" :label="__('Design')" />
                            </x-ui.sidebar.menu-item>
                            <x-ui.sidebar.menu-item>
                                <x-ui.sidebar.menu-button href="#" size="sm" :label="__('Content')" />
                            </x-ui.sidebar.menu-item>
                        </x-ui.sidebar.menu-sub>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Mobile app')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>
        </x-ui.sidebar.content>

        <x-ui.sidebar.footer>
            <x-ui.sidebar.menu-button href="#" :label="__('Settings')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></x-ui.sidebar.menu-button>
        </x-ui.sidebar.footer>
    </x-ui.sidebar>

    <x-ui.sidebar.inset variant="inset" class="min-w-0">
        <header class="flex h-12 shrink-0 items-center border-b border-border px-6">
            <h2 class="text-sm font-semibold text-foreground">{{ __('Dashboard') }}</h2>
        </header>
        <div class="flex-1 overflow-auto p-6">
            <p class="text-sm text-muted-foreground">{{ __('The rail sits on the canvas colour and the content is a raised panel. Collapse the rail with the trigger; the panel keeps its margin and rounded corners.') }}</p>
        </div>
    </x-ui.sidebar.inset>
</div>
mobile-bare.blade.php Blade
{{-- `bare` renders only the trigger and its sheet, so the trigger sits inside an app's own top
     bar next to the brand and the page actions. It is md:hidden in an app; `md:inline-flex!`
     only keeps it visible at every width on this page. --}}
@php
    $nav = [
        ['heading' => __('Workspace'), 'items' => [
            ['label' => __('Inbox'), 'href' => '#', 'current' => true],
            ['label' => __('Projects'), 'href' => '#'],
            ['label' => __('Reports'), 'href' => '#'],
        ]],
    ];
@endphp
<div class="w-full max-w-sm overflow-hidden rounded-lg border border-border bg-background">
    <header class="flex h-14 items-center gap-2 border-b border-border px-2">
        <x-ui.sidebar.mobile bare :nav="$nav" :label="__('Northwind Studio')" id="sidebar-mobile-bare-demo" class="md:inline-flex!">
            <x-slot:brand>
                <x-ui.brand as="span" size="sm" :label="__('Northwind Studio')" />
            </x-slot:brand>
        </x-ui.sidebar.mobile>
        <x-ui.brand size="sm" :label="__('Northwind Studio')" class="min-w-0" />
        <x-ui.button variant="outline" size="sm" class="ms-auto">{{ __('New project') }}</x-ui.button>
    </header>
    <p class="p-4 text-sm text-muted-foreground">{{ __('The trigger has no border, padding or label of its own.') }}</p>
</div>
mobile-brand.blade.php Blade
{{-- The phone bar with a brand lockup: the `brand` slot replaces the text label in the bar and
     in the sheet title, and `label` stays the sheet title's accessible name. The bar is md:hidden
     in an app; `md:flex!` only keeps it visible at every width on this page. --}}
@php
    $nav = [
        ['heading' => __('Workspace'), 'items' => [
            ['label' => __('Inbox'), 'href' => '#', 'current' => true, 'badge' => '4'],
            ['label' => __('Projects'), 'href' => '#'],
            ['label' => __('Reports'), 'href' => '#'],
        ]],
        ['heading' => __('Account'), 'items' => [
            ['label' => __('Settings'), 'href' => '#'],
        ]],
    ];
@endphp
<div class="w-full max-w-sm overflow-hidden rounded-lg border border-border bg-background">
    <x-ui.sidebar.mobile :nav="$nav" :label="__('Northwind Studio')" id="sidebar-mobile-brand-demo" class="md:flex!">
        <x-slot:brand>
            <x-ui.brand as="span" size="sm" :label="__('Northwind Studio')" />
        </x-slot:brand>
    </x-ui.sidebar.mobile>
    <p class="p-4 text-sm text-muted-foreground">{{ __('Open the menu: the sheet title shows the same lockup.') }}</p>
</div>
mobile-counts.blade.php Blade
{{-- The phone sheet shows the live counts the desktop rail shows: a nav
     item takes count, countExpr and countLabel, rendered by the same
     sidebar.menu-count part as a menu button. The bar is md:hidden in an
     app; `md:flex!` only keeps it visible at every width on this page. --}}
@php
    $nav = [
        ['heading' => __('Workspace'), 'items' => [
            ['label' => __('Inbox'), 'href' => '#', 'current' => true, 'count' => 3, 'countExpr' => 'unread', 'countLabel' => __('unread')],
            ['label' => __('Library'), 'href' => '#', 'count' => 128],
            ['label' => __('Archive'), 'href' => '#', 'count' => 0],
        ]],
    ];
@endphp
<div class="w-full max-w-sm overflow-hidden rounded-lg border border-border bg-background" x-data="{ unread: 3 }">
    <x-ui.sidebar.mobile :nav="$nav" :label="__('Workspace')" id="sidebar-mobile-counts-demo" class="md:flex!" />
    <div class="flex flex-wrap gap-2 p-4">
        <x-ui.button size="sm" variant="outline" x-on:click="unread++">{{ __('New item') }}</x-ui.button>
        <x-ui.button size="sm" variant="outline" x-on:click="unread = 0">{{ __('Mark all read') }}</x-ui.button>
    </div>
</div>
remembered-groups.blade.php Blade
{{-- Collapsible groups that remember their open state per browser. Fold a group, reload: it stays folded
     without a flash. Fold the sidebar: the rail still shows every row's icon. --}}
<div class="flex h-[32rem] w-full overflow-hidden rounded-lg border border-border" data-surface="admin">
    <x-ui.sidebar state-key="brok:docs:sidebar-remembered">
        <x-ui.sidebar.header>
            <x-ui.sidebar.trigger />
            <span x-show="expanded" class="truncate text-sm font-semibold text-foreground">{{ __('Acme Inc') }}</span>
        </x-ui.sidebar.header>

        <x-ui.sidebar.content>
            <x-ui.sidebar.group collapsible storage-key="brok:docs:sidebar-group:favorites">
                <x-ui.sidebar.group-label collapsible>{{ __('Favorites') }}</x-ui.sidebar.group-label>
                <x-ui.sidebar.menu :label="__('Favorites')">
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Website relaunch')"><x-ui.icon name="star-filled" /></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Quarterly invoices')"><x-ui.icon name="star-filled" /></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>

            <x-ui.sidebar.group collapsible :open="false" storage-key="brok:docs:sidebar-group:workspace">
                <x-ui.sidebar.group-label collapsible>{{ __('Workspace') }}</x-ui.sidebar.group-label>
                <x-ui.sidebar.menu :label="__('Workspace')">
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Projects')"><x-ui.icon name="folder" /></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Time')"><x-ui.icon name="clock" /></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Invoices')"><x-ui.icon name="receipt" /></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>
        </x-ui.sidebar.content>
    </x-ui.sidebar>

    <x-ui.sidebar.inset class="p-6">
        <h2 class="text-lg font-semibold text-foreground">{{ __('Remembered groups') }}</h2>
        <p class="mt-2 text-sm text-muted-foreground">{{ __('Workspace starts folded. Open or fold a group and reload: this browser keeps the choice.') }}</p>
    </x-ui.sidebar.inset>
</div>
shortcuts.blade.php Blade
{{-- Shortcut chips: `shortcut="mod+k"` shows ⌘ K on macOS and iOS and
     Ctrl K elsewhere, with the matching aria-keyshortcuts. The chips hide
     while the rail is collapsed and on a touch screen; bind the keys
     themselves with x-hotkey or the hotkeys store. --}}
<div class="flex h-[28rem] w-full overflow-hidden rounded-lg border border-border">
    <x-ui.sidebar>
        <x-ui.sidebar.header>
            <x-ui.sidebar.trigger />
            <span x-show="expanded" class="truncate text-sm font-semibold text-foreground">{{ __('Workspace') }}</span>
        </x-ui.sidebar.header>

        <x-ui.sidebar.content>
            <x-ui.sidebar.group>
                <x-ui.sidebar.menu>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button :label="__('Commands')" shortcut="mod+k"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :active="true" :label="__('Inbox')" shortcut="mod+1" :count="3" :count-label="__('unread')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('New capture')" shortcut="shift+n"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>
        </x-ui.sidebar.content>
    </x-ui.sidebar>

    <x-ui.sidebar.inset class="p-6">
        <p class="text-sm text-muted-foreground">{{ __('Collapse the rail to see the chips give way to the icons.') }}</p>
    </x-ui.sidebar.inset>
</div>
submit.blade.php Blade
{{-- A menu button that submits a form: Sign out is a POST, so the row is a
     `<button type="submit">` with the look of every other row. The
     `form="…"` attribute points it at a form outside the sidebar; a row
     inside a form needs no attribute. This page stops the submit and only
     says it happened. --}}
<div class="flex h-[24rem] w-full overflow-hidden rounded-lg border border-border" x-data="{ signedOut: false }">
    <x-ui.sidebar>
        <x-ui.sidebar.header>
            <x-ui.sidebar.trigger />
            <span x-show="expanded" class="truncate text-sm font-semibold text-foreground">{{ __('Workspace') }}</span>
        </x-ui.sidebar.header>

        <x-ui.sidebar.content>
            <x-ui.sidebar.group>
                <x-ui.sidebar.menu>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :active="true" :label="__('Inbox')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>
        </x-ui.sidebar.content>

        <x-ui.sidebar.footer>
            <x-ui.sidebar.menu-button type="submit" form="sidebar-sign-out-demo" :label="__('Sign out')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/></svg></x-ui.sidebar.menu-button>
        </x-ui.sidebar.footer>
    </x-ui.sidebar>

    <x-ui.sidebar.inset class="p-6">
        <x-ui.form id="sidebar-sign-out-demo" action="#" x-on:submit.prevent="signedOut = true" />
        <p class="text-sm text-muted-foreground" role="status" x-text="signedOut ? @js(__('The sign-out form was submitted.')) : @js(__('Sign out submits a form with a POST.'))">{{ __('Sign out submits a form with a POST.') }}</p>
    </x-ui.sidebar.inset>
</div>
variants.blade.php Blade
<div class="flex h-[32rem] w-full overflow-hidden rounded-lg border border-border" data-surface="admin">
    <x-ui.sidebar resizable :min-width="180" :max-width="360" :default-width="256" storage-key="demo:sidebar-width">
        <x-ui.sidebar.header>
            <x-ui.sidebar.trigger />
            <span x-show="expanded" class="truncate text-sm font-semibold text-foreground">{{ __('Acme Inc') }}</span>
        </x-ui.sidebar.header>

        <x-ui.sidebar.content>
            <x-ui.sidebar.group>
                <x-ui.sidebar.group-label>{{ __('Platform') }}</x-ui.sidebar.group-label>
                <x-ui.sidebar.menu>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :active="true" :label="__('Dashboard')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Inbox')" trailing="12"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Team')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Analytics')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>

            <x-ui.sidebar.group collapsible>
                <x-ui.sidebar.group-label collapsible>{{ __('Projects') }}</x-ui.sidebar.group-label>
                <x-ui.sidebar.menu :label="__('Projects')">
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Website relaunch')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></x-ui.sidebar.menu-button>
                        <x-ui.sidebar.menu-sub>
                            <x-ui.sidebar.menu-item>
                                <x-ui.sidebar.menu-button href="#" size="sm" :label="__('Design')" />
                            </x-ui.sidebar.menu-item>
                            <x-ui.sidebar.menu-item>
                                <x-ui.sidebar.menu-button href="#" size="sm" :label="__('Content')" />
                            </x-ui.sidebar.menu-item>
                        </x-ui.sidebar.menu-sub>
                    </x-ui.sidebar.menu-item>
                    <x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-button href="#" :label="__('Mobile app')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></x-ui.sidebar.menu-button>
                    </x-ui.sidebar.menu-item>
                </x-ui.sidebar.menu>
            </x-ui.sidebar.group>
        </x-ui.sidebar.content>

        <x-ui.sidebar.footer>
            <x-ui.sidebar.menu-button href="#" :label="__('Settings')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></x-ui.sidebar.menu-button>
        </x-ui.sidebar.footer>
    </x-ui.sidebar>

    <x-ui.sidebar.inset class="p-6">
        <h2 class="text-lg font-semibold text-foreground">{{ __('Dashboard') }}</h2>
        <p class="mt-2 text-sm text-muted-foreground">{{ __('Drag the edge of the sidebar to resize it, or focus the handle and use the arrow keys. The width is remembered.') }}</p>
    </x-ui.sidebar.inset>
</div>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
resizable bool false Adds a keyboard-operable separator on the inline-end edge that drags the expanded width between minWidth and maxWidth (Arrow keys step 8px, Shift 32px, Home/End to the limits, double-click resets).
minWidth int 200 Narrowest expanded width in px when resizable.
maxWidth int 480 Widest expanded width in px when resizable.
defaultWidth int 256 Expanded width in px before the visitor resizes, and the double-click reset target.
storageKey string | null null localStorage key that persists the resized width; null keeps the width for the session only. Reads and writes are guarded, so blocked storage never breaks the sidebar.
stateKey string sidebar:state localStorage key for the expanded/collapsed state. Give each shell its own key (for example brok:app-shell-06:sidebar) when one app renders several shells that should not follow each other.
variant sidebar | inset sidebar `sidebar` is a card-coloured rail with an inline-end border. `inset` puts the rail straight on the canvas colour with no border, for a shell whose content sits in a raised panel (pair it with `inset.variant`). Unknown values fall back to `sidebar`.
disabled false | true false Documented catalog control used by the preview workbench.
collapsible bool false Declared by @props in the registry Blade source.
open bool true Declared by @props in the registry Blade source.
bootSrc mixed | null null Declared by @props in the registry Blade source.
nonce mixed | null null Declared by @props in the registry Blade source.
label string Primary Declared by @props in the registry Blade source.
href mixed | null null Declared by @props in the registry Blade source.
active bool false Declared by @props in the registry Blade source.
size string md Declared by @props in the registry Blade source.
trailing mixed | null null Declared by @props in the registry Blade source.
count mixed | null null Declared by @props in the registry Blade source.
countExpr mixed | null null Declared by @props in the registry Blade source.
countLabel mixed | null null Declared by @props in the registry Blade source.
soon bool false Declared by @props in the registry Blade source.
type string button Declared by @props in the registry Blade source.
shortcut mixed | null null Declared by @props in the registry Blade source.
when mixed | null null Declared by @props in the registry Blade source.
nav array [] Declared by @props in the registry Blade source.
bare bool false Declared by @props in the registry Blade source.
id mixed | null null Declared by @props in the registry Blade source.

Slots

  • default — Compositional parts: header, content (with groups and menus), footer, and inset.
  • mobile.brand — `<x-ui.sidebar.mobile>`: an optional lockup (for example `<x-ui.brand as="span">`, a monogram and a name) shown in the bar instead of the text label and as the sheet title. `label` stays the sheet title's accessible name as screen-reader text; the lockup is rendered twice, so it must not carry ids.
  • mobile.footer — `<x-ui.sidebar.mobile>`: optional settings at the foot of the sheet, under the nav, for example `<x-ui.theme-switcher size="sm" />` so a phone reaches the theme. Rendered only when the slot has content (comments and Livewire block markers do not count). The theme switcher is a separate item: add it with `ui:add theme-switcher`.
  • x-ui.sidebar.trigger — Installed subcomponent from the registry item.
  • x-ui.sidebar.header — Installed subcomponent from the registry item.
  • x-ui.sidebar.content — Installed subcomponent from the registry item.
  • x-ui.sidebar.footer — Installed subcomponent from the registry item.
  • x-ui.sidebar.group — Installed subcomponent from the registry item.
  • x-ui.sidebar.group-label — Installed subcomponent from the registry item.
  • x-ui.sidebar.menu — Installed subcomponent from the registry item.
  • x-ui.sidebar.menu-item — Installed subcomponent from the registry item.
  • x-ui.sidebar.menu-button — Installed subcomponent from the registry item.
  • x-ui.sidebar.separator — Installed subcomponent from the registry item.
  • x-ui.sidebar.inset — Installed subcomponent from the registry item.
  • x-ui.sidebar.menu-sub — Installed subcomponent from the registry item.
  • x-ui.sidebar.menu-count — Installed subcomponent from the registry item.
  • x-ui.sidebar.mobile — Installed subcomponent from the registry item.

Data slots

Stable hooks for CSS overrides and browser tests.

sidebar sidebar-content sidebar-footer sidebar-group sidebar-group-boot sidebar-group-label sidebar-header sidebar-inset sidebar-menu sidebar-menu-button sidebar-menu-count sidebar-menu-item sidebar-menu-shortcut sidebar-menu-soon sidebar-menu-sub sidebar-mobile sidebar-mobile-brand sidebar-mobile-footer sidebar-resize-handle sidebar-separator sidebar-trigger

Behavior

  • Both parts carry `data-variant` (`sidebar` or `inset`), so a shell can style its own regions from the chosen variant.
  • `<x-ui.sidebar.mobile>` renders the `md:hidden` bar + start-side sheet that stands in for the desktop rail below the medium breakpoint; pass a default slot to render custom content instead of the generated nav list. `bare` drops the bar chrome so the trigger can sit in an existing top bar; the `brand` slot replaces the text label in the bar and the sheet title.
  • A folded collapsible group hides its menu only while the sidebar is expanded: in the collapsed icon rail, where group labels are hidden, every row keeps its icon. The fold follows the group's `data-open` and the sidebar's `data-state`, so the server-rendered and remembered state is right before Alpine starts.
  • Menu button counts: count / count-expr render a tabular number at the row end (data-slot="sidebar-menu-count"), hidden at zero and while the rail is collapsed; count-expr keeps it live from Alpine or Livewire state. count-label adds visually hidden words after the number.
  • Menu button type: without an href the row is a <button type="button">; type="submit" (with an optional form="…" attribute) makes it a submit button for a POST action such as sign out. A disabled or soon submit row is a disabled button, so it never submits.
  • Mobile counts: a mobile.nav item with count / countExpr / countLabel renders the same data-slot="sidebar-menu-count" markup as a desktop menu button (through sidebar.menu-count), with the same live expression, zero hiding and screen-reader words. Custom sheet content can also use sidebar.menu-button rows, which read as expanded outside the rail.
  • Menu button shortcut: shortcut renders data-slot="sidebar-menu-shortcut" with one <kbd> per key (left to right, also in RTL) after the label, hidden while the rail is collapsed, on a touch screen (pointer: coarse) and on a disabled or soon row. The server names mod from Sec-CH-UA-Platform, then the User-Agent; a page in a shared full-page cache must vary on the platform.
  • Menu button disabled / soon: the row stays in the list with its icon and name but is not a working target: a link has no href, aria-disabled="true" and tabindex="-1"; a button is disabled. soon adds an outline Soon badge (data-slot="sidebar-menu-soon") while expanded.
  • Persists expanded/collapsed state and exposes data-state for responsive layout styling.
  • Trigger updates aria-expanded and the menu remains navigable when collapsed.
  • Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
  • Declares registry capability flags: a11y, interactive, reducedMotion, responsive, rtl, darkMode, localized.

Guidance

Navigation and orientation

Orient users and move between destinations.

Use when

  • Use to orient users and help them move across pages, sections, or commands.
  • Building an application shell with persistent side navigation, grouped menu items, and a collapsible rail.
  • Needing the sidebar's expanded/collapsed state and width to persist across visits via localStorage.

Avoid when

  • Do not hide primary wayfinding in novelty interactions or deep nested structures if straightforward navigation would be clearer.
  • The navigation is a small, flat set of links; use navigation-menu or a plain nav instead of the full app-shell sidebar.
  • The page is a marketing or docs page rather than an authenticated app shell; use table-of-contents or a simple nav.

Use instead

  • Visible links and local navigation

Anti-patterns

  • Hiding primary wayfinding in novelty interactions
Anatomy
root trigger header content group group-label menu menu-item menu-button menu-count menu-sub separator footer inset mobile
Theming hooks
sidebar sidebar-content sidebar-footer sidebar-group sidebar-group-boot sidebar-group-label sidebar-header sidebar-inset sidebar-menu sidebar-menu-button sidebar-menu-count sidebar-menu-item sidebar-menu-shortcut sidebar-menu-soon sidebar-menu-sub sidebar-mobile sidebar-mobile-brand sidebar-mobile-footer sidebar-resize-handle sidebar-separator sidebar-trigger

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
ArrowUp ArrowDown ArrowLeft ArrowRight Home End
Focus
managed
  • A menu button shortcut is exposed as aria-keyshortcuts on the row (Meta+K on Apple platforms, Control+K elsewhere for mod+k) and the visible keys are aria-hidden, so the row name stays the label.
  • The resize handle is keyboard-operable (Arrow keys step 8px, Shift 32px, Home/End to the limits, double-click resets), so width is adjustable without a pointer.
  • Below the medium breakpoint the sidebar becomes a sheet with the same accessible trigger and focus-trap behaviour as any other sheet, rather than a cramped rail.
  • With a `brand` slot the sheet title reads the `label` text (screen-reader only) and hides the visual lockup from assistive technology, so the dialog keeps a plain-text name.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="sidebar-{{ $record->id }}">
    <div class="flex h-[32rem] w-full overflow-hidden rounded-lg border border-border" data-surface="admin">
        <x-ui.sidebar>
            <x-ui.sidebar.header>
                <x-ui.sidebar.trigger />
                <span x-show="expanded" class="truncate text-sm font-semibold text-foreground">{{ __('Acme Inc') }}</span>
            </x-ui.sidebar.header>
    
            <x-ui.sidebar.content>
                <x-ui.sidebar.group>
                    <x-ui.sidebar.group-label>{{ __('Platform') }}</x-ui.sidebar.group-label>
                    <x-ui.sidebar.menu>
                        <x-ui.sidebar.menu-item>
                            <x-ui.sidebar.menu-button href="#" :active="true" :label="__('Dashboard')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg></x-ui.sidebar.menu-button>
                        </x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-item>
                            <x-ui.sidebar.menu-button href="#" :label="__('Inbox')" trailing="12"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg></x-ui.sidebar.menu-button>
                        </x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-item>
                            <x-ui.sidebar.menu-button href="#" :label="__('Team')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></x-ui.sidebar.menu-button>
                        </x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-item>
                            <x-ui.sidebar.menu-button href="#" :label="__('Analytics')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg></x-ui.sidebar.menu-button>
                        </x-ui.sidebar.menu-item>
                    </x-ui.sidebar.menu>
                </x-ui.sidebar.group>
    
                <x-ui.sidebar.group collapsible>
                    <x-ui.sidebar.group-label collapsible>{{ __('Projects') }}</x-ui.sidebar.group-label>
                    <x-ui.sidebar.menu :label="__('Projects')">
                        <x-ui.sidebar.menu-item>
                            <x-ui.sidebar.menu-button href="#" :label="__('Website relaunch')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></x-ui.sidebar.menu-button>
                            <x-ui.sidebar.menu-sub>
                                <x-ui.sidebar.menu-item>
                                    <x-ui.sidebar.menu-button href="#" size="sm" :label="__('Design')" />
                                </x-ui.sidebar.menu-item>
                                <x-ui.sidebar.menu-item>
                                    <x-ui.sidebar.menu-button href="#" size="sm" :label="__('Content')" />
                                </x-ui.sidebar.menu-item>
                            </x-ui.sidebar.menu-sub>
                        </x-ui.sidebar.menu-item>
                        <x-ui.sidebar.menu-item>
                            <x-ui.sidebar.menu-button href="#" :label="__('Mobile app')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></x-ui.sidebar.menu-button>
                        </x-ui.sidebar.menu-item>
                    </x-ui.sidebar.menu>
                </x-ui.sidebar.group>
            </x-ui.sidebar.content>
    
            <x-ui.sidebar.footer>
                <x-ui.sidebar.menu-button href="#" :label="__('Settings')"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></x-ui.sidebar.menu-button>
            </x-ui.sidebar.footer>
        </x-ui.sidebar>
    
        <x-ui.sidebar.inset class="p-6">
            <h2 class="text-lg font-semibold text-foreground">{{ __('Dashboard') }}</h2>
            <p class="mt-2 text-sm text-muted-foreground">{{ __('Fold the sidebar from the trigger; the rail keeps every destination as a centred icon with its name as a title.') }}</p>
        </x-ui.sidebar.inset>
    </div>
</div>

Validation

Validation support: native. Keep the error message connected with aria-describedby.

livewire-form.blade.php Blade
<form wire:submit="save" class="space-y-2">
    <brok:sidebar
        wire:model="value"
        :aria-invalid="$errors->has('value') ? 'true' : 'false'"
        aria-describedby="value-error"
    />

    @error('value')
        <p id="value-error" role="alert">{{ $message }}</p>
    @enderror

    <brok:button type="submit" wire:loading.attr="disabled">
        <span wire:loading.remove>Save</span>
        <span wire:loading>Saving…</span>
    </brok:button>
</form>

Source

The exact, editable files ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/sidebar.blade.php Blade
@props([
    'resizable' => false,
    'minWidth' => 200,
    'maxWidth' => 480,
    'defaultWidth' => 256,
    'storageKey' => null,
    // localStorage key for the expanded/collapsed state. Every sidebar shares
    // `sidebar:state` by default; give each shell its own key when one app
    // renders several shells that should not follow each other.
    'stateKey' => 'sidebar:state',
    'variant' => 'sidebar',
])

@php
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $variant = $styles['normalizeVariant']($variant) === 'inset' ? 'inset' : 'sidebar';
    $surface = $variant === 'inset'
        ? 'bg-canvas text-foreground'
        : 'border-e border-border bg-card text-card-foreground';
@endphp

<aside
    x-data="uiSidebar({
        resizable: @js((bool) $resizable),
        minWidth: @js((int) $minWidth),
        maxWidth: @js((int) $maxWidth),
        defaultWidth: @js((int) $defaultWidth),
        storageKey: @js($storageKey),
        stateKey: @js((string) $stateKey),
        resizeLabel: @js(__('Resize sidebar')),
    })"
    x-ref="sidebar"
    data-slot="sidebar"
    data-variant="{{ $variant }}"
    {{-- Server-rendered so the width is never `auto` before Alpine starts. --}}
    data-state="expanded"
    :data-state="expanded ? 'expanded' : 'collapsed'"
    :data-resizable="resizable ? 'true' : 'false'"
    :style="(resizable && expanded) ? ('--sidebar-width: ' + width + 'px') : ''"
    {{ $attributes->merge(['class' => 'group/sidebar relative flex h-full flex-col '.$surface.' duration-200 data-[sidebar-motion=ready]:transition-[width] data-[state=collapsed]:w-16 data-[state=expanded]:w-64 data-[resizable=true]:data-[state=expanded]:w-[var(--sidebar-width)] motion-reduce:transition-none']) }}
>
    {{ $slot }}

    <template x-if="resizable">
        <div
            data-slot="sidebar-resize-handle"
            x-show="expanded"
            role="separator"
            aria-orientation="vertical"
            :aria-label="resizeLabel"
            :aria-valuemin="minWidth"
            :aria-valuemax="maxWidth"
            :aria-valuenow="width"
            :tabindex="expanded ? 0 : -1"
            @pointerdown="startResize($event)"
            @keydown="onResizeKey($event)"
            @dblclick="resetWidth()"
            class="group absolute inset-y-0 end-0 z-20 flex w-2 translate-x-1/2 cursor-col-resize touch-none items-center justify-center focus-visible:outline-none rtl:-translate-x-1/2"
        >
            <span
                aria-hidden="true"
                class="h-full w-0.5 bg-border transition-colors group-hover:bg-ring group-focus-visible:bg-ring group-focus-visible:ring-2 group-focus-visible:ring-ring motion-reduce:transition-none"
            ></span>
        </div>
    </template>
</aside>
resources/views/components/ui/sidebar/trigger.blade.php Blade
<button
    type="button"
    data-slot="sidebar-trigger"
    @click="toggle()"
    :aria-expanded="expanded"
    aria-label="{{ __('Toggle sidebar') }}"
    {{ $attributes->merge(['class' => 'inline-flex size-8 items-center justify-center rounded-md text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none']) }}
>
    <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>
</button>
resources/views/components/ui/sidebar/header.blade.php Blade
<div
    data-slot="sidebar-header"
    {{ $attributes->merge(['class' => 'flex items-center gap-2 p-2 group-data-[state=collapsed]/sidebar:justify-center']) }}
>
    {{ $slot }}
</div>
resources/views/components/ui/sidebar/content.blade.php Blade
<div
    data-slot="sidebar-content"
    {{ $attributes->merge(['class' => 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto p-2']) }}
>
    {{ $slot }}
</div>
resources/views/components/ui/sidebar/footer.blade.php Blade
<div
    data-slot="sidebar-footer"
    {{ $attributes->merge(['class' => 'flex flex-col gap-2 p-2 group-data-[state=collapsed]/sidebar:items-center']) }}
>
    {{ $slot }}
</div>
resources/views/components/ui/sidebar/group.blade.php Blade
@props([
    // Collapsible section: the label becomes a disclosure button with a
    // chevron and the rows fold away. `open` sets the initial state.
    'collapsible' => false,
    'open' => true,
    // localStorage key that remembers the open state per browser (stored as
    // `open` or `closed`); null keeps it for the page only. A blocking file
    // script applies the stored state while the page parses, so a folded
    // group never flashes open.
    'storageKey' => null,
    // URL and CSP nonce of that boot script, like theme-script.
    'bootSrc' => null,
    'nonce' => null,
])

@php
    $remembers = $collapsible && filled($storageKey);
    $bootSrc ??= asset('vendor/brok/sidebar-group-boot.js');
@endphp

<div
    data-slot="sidebar-group"
    @if ($collapsible)
        x-data="uiSidebarGroup({ open: @js((bool) $open), storageKey: @js($remembers ? (string) $storageKey : null) })"
        data-open="{{ $open ? 'true' : 'false' }}"
        :data-open="groupOpen ? 'true' : 'false'"
        @if ($remembers) data-storage-key="{{ $storageKey }}" @endif
    @endif
    {{ $attributes->merge(['class' => 'group/sidebar-group flex flex-col gap-1']) }}
>
    @if ($remembers)
        <script data-slot="sidebar-group-boot" src="{{ $bootSrc }}" @if (filled($nonce)) nonce="{{ $nonce }}" @endif></script>
    @endif
    {{ $slot }}
</div>
resources/views/components/ui/sidebar/group-label.blade.php Blade
@props([
    // Set on the label of a collapsible group: renders it as the disclosure
    // button that folds the group's rows.
    'collapsible' => false,
])

@php
    $base = 'flex h-8 items-center gap-1 px-2 text-xs font-medium text-muted-foreground';
@endphp

@if ($collapsible)
    <button
        type="button"
        data-slot="sidebar-group-label"
        x-show="expanded"
        @click="groupOpen = !groupOpen"
        :aria-expanded="groupOpen"
        {{ $attributes->merge(['class' => $base.' w-full rounded-md outline-none transition-colors hover:text-foreground focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none']) }}
    >
        <span class="min-w-0 flex-1 truncate text-start">{{ $slot }}</span>
        <svg class="size-4 shrink-0 transition-transform group-data-[open=false]/sidebar-group:-rotate-90 motion-reduce:transition-none rtl:group-data-[open=false]/sidebar-group:rotate-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
    </button>
@else
    <div
        data-slot="sidebar-group-label"
        x-show="expanded"
        {{ $attributes->merge(['class' => $base]) }}
    >
        {{ $slot }}
    </div>
@endif
resources/views/components/ui/sidebar/menu.blade.php Blade
@props(['label' => 'Primary'])

{{-- In a folded collapsible group the rows hide, except in the collapsed icon
     rail: the rail has no group labels to reopen a group, so it keeps every
     icon. Driven by data attributes, so the server-rendered and boot-script
     state is right before Alpine starts. --}}

<nav aria-label="{{ __($label) }}" data-slot="sidebar-menu" {{ $attributes->merge(['class' => 'flex flex-col gap-1 group-data-[open=false]/sidebar-group:not-group-data-[state=collapsed]/sidebar:hidden']) }}>
    <ul class="flex flex-col gap-1">
        {{ $slot }}
    </ul>
</nav>
resources/views/components/ui/sidebar/menu-item.blade.php Blade
<li
    data-slot="sidebar-menu-item"
    {{ $attributes->merge(['class' => 'relative']) }}
>
    {{ $slot }}
</li>
resources/views/components/ui/sidebar/menu-button.blade.php Blade
@props([
    'href' => null,
    'active' => false,
    // Row density: md is the app-shell rail (40px), sm the dense docs/tree rail.
    'size' => 'md',
    // The row's text. With `label`, the slot is the icon: the label hides when
    // the sidebar collapses (kept for assistive tech, and as the row's title),
    // so a collapsed rail is a centred column of icons with names.
    'label' => null,
    // Trailing content (a count badge, a shortcut) shown while expanded.
    'trailing' => null,
    // A number shown at the row end (unread items, open tasks). Zero, null or
    // an empty value hides it. `count-expr` is an Alpine expression for a
    // live count ($store.inbox.unread, a Livewire-entangled property); the
    // server `count` is its first value. `count-label` is read after the
    // number by screen readers ("12 unread").
    'count' => null,
    'countExpr' => null,
    'countLabel' => null,
    // A destination that cannot be used yet: the row keeps its place and name
    // but is not a link or button target (aria-disabled, out of the tab
    // order). `soon` also marks it with a "Soon" badge (true, or the badge
    // text) and implies disabled.
    'disabled' => false,
    'soon' => false,
    // Button type when the row has no href: `submit` (or `reset`) makes the
    // row submit its enclosing form, or the form named by a `form="…"`
    // attribute (a sign-out POST), with the look of every other row.
    'type' => 'button',
    // A keyboard shortcut hint, keys joined by "+" ("mod+k", "shift+n"):
    // drawn as <kbd> keys at the row end while the rail is
    // expanded and exposed as aria-keyshortcuts. `mod` is the platform's
    // command key, as in button: ⌘ (Meta) for a request from macOS or iOS,
    // else Ctrl (Control). A hint only; bind the key with x-hotkey or the
    // hotkeys store. Hidden on a touch screen (pointer: coarse).
    'shortcut' => null,
])

@php
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $sidebar = $styles['sidebar'];
    $classes = trim($sidebar['menuButton'].' '.($sidebar['menuButtonSizes'][$size] ?? $sidebar['menuButtonSizes']['md']));
    $soonLabel = match (true) {
        $soon === true, $soon === 'true' => __('Soon'),
        is_string($soon) && trim($soon) !== '' && $soon !== 'false' => $soon,
        default => null,
    };
    $isDisabled = ($disabled === true || $disabled === 'true') || $soonLabel !== null;
    $tag = $href ? 'a' : 'button';
    $buttonType = in_array($type, ['submit', 'reset'], true) ? $type : 'button';
    $expanded = 'typeof expanded === \'undefined\' || expanded';

    $shortcutKeys = [];
    $shortcutAria = null;
    if (filled($shortcut)) {
        $keyNames = [
            'ctrl' => ['Ctrl', 'Control'], 'control' => ['Ctrl', 'Control'], 'cmd' => ['⌘', 'Meta'], 'meta' => ['⌘', 'Meta'],
            'alt' => ['Alt', 'Alt'], 'option' => ['⌥', 'Alt'], 'shift' => ['Shift', 'Shift'], 'enter' => ['Enter', 'Enter'],
            'escape' => ['Esc', 'Escape'], 'esc' => ['Esc', 'Escape'], 'space' => ['Space', 'Space'], 'tab' => ['Tab', 'Tab'],
            'backspace' => ['Backspace', 'Backspace'], 'delete' => ['Delete', 'Delete'],
            'up' => ['↑', 'ArrowUp'], 'down' => ['↓', 'ArrowDown'], 'left' => ['←', 'ArrowLeft'], 'right' => ['→', 'ArrowRight'],
        ];
        $shortcutParts = array_filter(explode('+', trim((string) $shortcut)), 'strlen');
        if (in_array('mod', array_map('mb_strtolower', $shortcutParts), true)) {
            // The server picks the label for `mod`, as button does:
            // Sec-CH-UA-Platform first (Chromium), then the User-Agent. A page
            // in a shared full-page cache must vary on it, or name the key.
            $platformHint = trim((string) request()->header('Sec-CH-UA-Platform'), "\" ");
            $isApplePlatform = $platformHint !== ''
                ? in_array(mb_strtolower($platformHint), ['macos', 'ios'], true)
                : preg_match('/Macintosh|Mac OS X|iPhone|iPad|iPod/i', (string) request()->userAgent()) === 1;
            $keyNames['mod'] = $isApplePlatform ? ['⌘', 'Meta'] : ['Ctrl', 'Control'];
        }
        $ariaParts = [];
        foreach ($shortcutParts as $part) {
            [$keyLabel, $keyAria] = $keyNames[mb_strtolower($part)] ?? [mb_strtoupper($part), mb_strtoupper($part)];
            $shortcutKeys[] = $keyLabel;
            $ariaParts[] = $keyAria;
        }
        $shortcutAria = $ariaParts === [] ? null : implode('+', $ariaParts);
    }
    // A consumer's own aria-keyshortcuts (several chords, "Meta+K Control+K") wins.
    if ($attributes->has('aria-keyshortcuts')) {
        $shortcutAria = null;
    }
@endphp

<{{ $tag }}
    @if ($tag === 'a')
        @if ($isDisabled) aria-disabled="true" tabindex="-1" @else href="{{ $href }}" @endif
    @else
        type="{{ $buttonType }}"
        @disabled($isDisabled)
    @endif
    data-slot="sidebar-menu-button"
    data-active="{{ $active ? 'true' : 'false' }}"
    @if ($isDisabled) data-disabled="true" @endif
    @if ($soonLabel !== null) data-soon="true" @endif
    @if ($active) aria-current="page" @endif
    @if ($shortcutAria !== null && ! $isDisabled) aria-keyshortcuts="{{ $shortcutAria }}" @endif
    @if ($label !== null) :title="typeof expanded === 'undefined' || expanded ? null : @js($label)" @endif
    {{ $attributes->class(['text-start' => $tag === 'button', 'cursor-not-allowed opacity-60 hover:bg-transparent' => $isDisabled])->merge(['class' => $classes]) }}
>
    {{ $slot }}
    @if ($label !== null)
        <span class="min-w-0 flex-1 truncate" :class="typeof expanded !== 'undefined' && !expanded ? 'sr-only' : ''">{{ $label }}</span>
        @if ($trailing)
            <span class="ms-auto shrink-0 text-xs text-muted-foreground" x-show="typeof expanded === 'undefined' || expanded">{{ $trailing }}</span>
        @endif
    @endif
    @if ($shortcutKeys !== [] && ! $isDisabled)
        <span
            data-slot="sidebar-menu-shortcut"
            dir="ltr"
            aria-hidden="true"
            x-show="{{ $expanded }}"
            @class(['inline-flex shrink-0 items-center gap-1 pointer-coarse:hidden', 'ms-auto' => ! ($label !== null && $trailing)])
        >@foreach ($shortcutKeys as $key)<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded-sm border border-border bg-background px-1 font-body text-xs font-medium leading-none text-muted-foreground">{{ $key }}</kbd>@endforeach</span>
    @endif
    @if ($soonLabel !== null)
        <span data-slot="sidebar-menu-soon" class="ms-auto shrink-0" x-show="{{ $expanded }}"><x-ui.badge variant="outline" size="sm">{{ $soonLabel }}</x-ui.badge></span>
    @else
        <x-ui.sidebar.menu-count :count="$count" :count-expr="$countExpr" :count-label="$countLabel" :when="$expanded" />
    @endif
</{{ $tag }}>
resources/views/components/ui/sidebar/separator.blade.php Blade
<div
    data-slot="sidebar-separator"
    role="separator"
    {{ $attributes->merge(['class' => 'mx-2 h-px bg-border']) }}
></div>
resources/views/components/ui/sidebar/inset.blade.php Blade
@props([
    'variant' => 'sidebar',
])

@php
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $variant = $styles['normalizeVariant']($variant) === 'inset' ? 'inset' : 'sidebar';
    $surface = $variant === 'inset'
        ? ' bg-background md:my-2 md:me-2 md:overflow-hidden md:rounded-lg md:border md:border-border md:shadow-xs'
        : '';
@endphp

<main
    data-slot="sidebar-inset"
    data-variant="{{ $variant }}"
    {{ $attributes->merge(['class' => 'flex flex-1 flex-col'.$surface]) }}
>
    {{ $slot }}
</main>
resources/js/ui/sidebar.js JS
/**
 * Sidebar behavior (spec §19 behavior layer).
 *
 * Holds the expanded/collapsed state on the root component and persists it to
 * localStorage under `stateKey` (`sidebar:state` by default) so the chosen
 * layout survives navigation and reloads; a shell passes its own key when
 * several shells in one app should not follow each other.
 *
 * When the sidebar is `resizable`, it also tracks the expanded `width` (px,
 * clamped to [minWidth, maxWidth]). A focusable `role="separator"` handle on the
 * inline-END edge drives the width via pointer drag or Arrow keys; the chosen
 * width persists to localStorage under `storageKey` when one is provided and is
 * restored on init. Dragging is direction-aware: under `dir="rtl"` the inline
 * end is on the left, so the geometry mirrors and the handle always tracks the
 * pointer. All drag listeners are removed in `destroy()` so unmounting mid-drag
 * cannot leak `pointermove`/`pointerup` handlers.
 *
 * `uiSidebarGroup` backs a collapsible `sidebar.group`; with a `storageKey` it
 * remembers the group's open state per browser.
 *
 * All localStorage access is guarded for privacy-mode / SSR safety.
 *
 * Self-registers on `alpine:init` so import order does not matter — import this
 * file once from your bundle (e.g. resources/js/ui/index.js).
 */
// localStorage throws in private windows and when site data is blocked. Both
// helpers swallow ONLY that — a persisted-state failure must never break the
// sidebar — and keep the policy in one place instead of four try/catch copies.
function readStorage(key) {
    try {
        return window.localStorage.getItem(key);
    } catch {
        return null;
    }
}

function writeStorage(key, value) {
    try {
        window.localStorage.setItem(key, value);
    } catch {
        // Session-only: nothing to do.
    }
}

// A remembered group state is stored as 'open' or 'closed'; 'true' and
// 'false' are read too. sidebar-group-boot.js applies the same rule before
// Alpine starts (SidebarGroupStateTest keeps the two in step).
function readGroupOpen(key, fallback) {
    const value = readStorage(key);
    if (value === 'open' || value === 'true') return true;
    if (value === 'closed' || value === 'false') return false;
    return fallback;
}

document.addEventListener('alpine:init', () => {
    // A collapsible `sidebar.group`: `groupOpen` folds its menu. With a
    // `storageKey` the state is read before the first binding (so Alpine never
    // flips what the boot script applied) and written on every change.
    window.Alpine.data('uiSidebarGroup', (config = {}) => {
        const groupStorageKey = config.storageKey || null;
        const open = config.open ?? true;

        return {
            groupOpen: groupStorageKey ? readGroupOpen(groupStorageKey, open) : open,
            groupStorageKey,

            init() {
                if (!this.groupStorageKey) return;
                this.$watch('groupOpen', (value) => {
                    writeStorage(this.groupStorageKey, value ? 'open' : 'closed');
                });
            },
        };
    });

    window.Alpine.data('uiSidebar', (config = {}) => ({
        expanded: true,

        // Resize config (only meaningful when `resizable` is true).
        resizable: config.resizable ?? false,
        minWidth: config.minWidth ?? 200,
        maxWidth: config.maxWidth ?? 480,
        defaultWidth: config.defaultWidth ?? 256,
        storageKey: config.storageKey ?? null,
        stateKey: config.stateKey || 'sidebar:state',
        width: config.defaultWidth ?? 256,
        resizeLabel: config.resizeLabel ?? 'Resize sidebar',

        // Live drag listeners, tracked so destroy() can detach them.
        _onMove: null,
        _onUp: null,

        init() {
            // Storage can be unavailable (private mode, blocked site data): the
            // sidebar then simply keeps its server-rendered state for the session.
            const stored = readStorage(this.stateKey);
            if (stored !== null) {
                this.expanded = stored === 'true';
            }

            if (this.resizable) {
                this.width = this.clampWidth(this.defaultWidth);
                if (this.storageKey) {
                    const parsed = parseInt(readStorage(this.storageKey) ?? '', 10);
                    if (!Number.isNaN(parsed)) {
                        this.width = this.clampWidth(parsed);
                    }
                }
            }

            // The restored state paints once without motion; the width
            // transition turns on the frame after, so only a user's toggle
            // animates, not the page load.
            requestAnimationFrame(() => requestAnimationFrame(() => {
                this.$el.dataset.sidebarMotion = 'ready';
            }));
        },

        toggle() {
            this.expanded = !this.expanded;
            writeStorage(this.stateKey, String(this.expanded));
        },

        clampWidth(w) {
            return Math.min(this.maxWidth, Math.max(this.minWidth, Math.round(w)));
        },

        persistWidth() {
            if (!this.storageKey) return;
            writeStorage(this.storageKey, String(this.width));
        },

        applyWidth(w) {
            this.width = this.clampWidth(w);
        },

        resetWidth() {
            this.applyWidth(this.defaultWidth);
            this.persistWidth();
        },

        startResize(e) {
            if (!this.resizable || !this.expanded) return;
            e.preventDefault();
            const rtl = getComputedStyle(this.$refs.sidebar).direction === 'rtl';
            const startX = e.clientX;
            const startWidth = this.width;

            this._onMove = (ev) => {
                // Pointer moving toward the inline-END edge widens the sidebar.
                // In LTR the inline end is on the right (delta = ev - start);
                // in RTL it is on the left, so the delta is inverted.
                let delta = ev.clientX - startX;
                if (rtl) delta = -delta;
                this.applyWidth(startWidth + delta);
            };
            this._onUp = () => {
                this.persistWidth();
                this._teardownDrag();
            };

            window.addEventListener('pointermove', this._onMove);
            window.addEventListener('pointerup', this._onUp);
        },

        onResizeKey(e) {
            if (!this.resizable || !this.expanded) return;
            const rtl = getComputedStyle(this.$refs.sidebar).direction === 'rtl';
            const step = e.shiftKey ? 32 : 8;
            const dec = () => this.applyWidth(this.width - step);
            const inc = () => this.applyWidth(this.width + step);
            let handled = true;
            switch (e.key) {
                // Toward the inline-end edge (= wider) is Right in LTR, Left in RTL.
                case 'ArrowLeft':
                    rtl ? inc() : dec();
                    break;
                case 'ArrowRight':
                    rtl ? dec() : inc();
                    break;
                case 'ArrowUp':
                    inc();
                    break;
                case 'ArrowDown':
                    dec();
                    break;
                case 'Home':
                    this.applyWidth(this.minWidth);
                    break;
                case 'End':
                    this.applyWidth(this.maxWidth);
                    break;
                default:
                    handled = false;
            }
            if (handled) {
                e.preventDefault();
                this.persistWidth();
            }
        },

        _teardownDrag() {
            if (this._onMove) {
                window.removeEventListener('pointermove', this._onMove);
                this._onMove = null;
            }
            if (this._onUp) {
                window.removeEventListener('pointerup', this._onUp);
                this._onUp = null;
            }
        },

        destroy() {
            this._teardownDrag();
        },
    }));
});
resources/views/components/ui/sidebar/menu-sub.blade.php Blade
@php
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
@endphp

{{-- Nested rows under a parent row: a hairline down the start edge, hidden
     while the sidebar is collapsed. --}}
<ul
    data-slot="sidebar-menu-sub"
    x-show="typeof expanded === 'undefined' || expanded"
    {{ $attributes->merge(['class' => $styles['sidebar']['menuSub']]) }}
>
    {{ $slot }}
</ul>
resources/views/components/ui/sidebar/menu-count.blade.php Blade
@props([
    // A number shown at the row end. Zero, null or an empty value hides it.
    'count' => null,
    // An Alpine expression for a live count ($store.inbox.unread, a
    // Livewire-entangled property); `count` is its first server value.
    'countExpr' => null,
    // Words read after the number by screen readers ("12 unread").
    'countLabel' => null,
    // An extra Alpine condition the count also needs to show. The desktop rail
    // passes its expanded state; the mobile sheet passes none.
    'when' => null,
])

{{-- The row-end count that sidebar.menu-button and the sidebar.mobile nav
     share, so both evaluate the live expression and name the number the
     same way. Renders nothing without a count or an expression. --}}
@php
    $initialCount = is_numeric($count) ? (int) $count : 0;
    $isLive = filled($countExpr);
    $condition = filled($when) ? (string) $when : null;
@endphp

@if ($isLive || $initialCount > 0)
    <span
        data-slot="sidebar-menu-count"
        {{ $attributes->merge(['class' => 'ms-auto shrink-0 text-xs tabular-nums text-muted-foreground']) }}
        @if ($isLive)
            x-data="{ get count() { const value = Number({{ $countExpr }}); return Number.isFinite(value) && value > 0 ? value : 0 } }"
            x-show="{{ $condition !== null ? '('.$condition.') && ' : '' }}count > 0"
        @elseif ($condition !== null)
            x-show="{{ $condition }}"
        @endif
        @if ($initialCount <= 0) style="display: none" @endif
    ><span @if ($isLive) x-text="count" @endif>{{ $initialCount > 0 ? $initialCount : '' }}</span>@if (filled($countLabel))<span class="sr-only"> {{ __($countLabel) }}</span>@endif</span>
@endif
resources/views/components/ui/sidebar/mobile.blade.php Blade
@props([
    // Groups of { heading, items: [{ label, href, icon?, current?, badge?,
    // count?, countExpr?, countLabel? }] }. `icon`, when present, is trusted
    // raw SVG path/shape markup (see the shell's own icon map) rendered inside
    // a 24x24 viewBox — never feed user input through it. `count`,
    // `countExpr` and `countLabel` render the same live row-end count as
    // sidebar.menu-button. Ignored once a default slot is supplied.
    'nav' => [],
    // Sheet title, usually the org/workspace name. With a `brand` slot it
    // stays the accessible name of the sheet title (screen-reader only).
    'label' => null,
    // Bare: only the trigger and its sheet, with no bar chrome (no border,
    // padding or label), for placing the trigger inside an existing top bar.
    'bare' => false,
    // Sheet content id (and the trigger's aria-controls). Auto-generated when omitted.
    'id' => null,
])

@php
    $id ??= 'sidebar-mobile-'.\Illuminate\Support\Str::random(6);
    $bare = filter_var($bare, FILTER_VALIDATE_BOOLEAN);
    // Optional named slot: a visual lockup (monogram + name) shown in the bar
    // and as the sheet title instead of the plain text label.
    $hasBrand = isset($brand) && trim((string) $brand) !== '';
    // Optional named slot: settings at the foot of the sheet, under the nav —
    // for example <x-ui.theme-switcher size="sm" />, which is not reachable
    // from the collapsed desktop sidebar on a phone.
    $hasFooter = isset($footer) && ($footer instanceof \Illuminate\View\ComponentSlot ? $footer->hasActualContent() : trim((string) $footer) !== '');
@endphp

<div
    data-slot="sidebar-mobile"
    @if ($bare) data-bare="true" @endif
    {{ $attributes->merge(['class' => $bare ? 'inline-flex shrink-0 items-center md:hidden' : 'flex items-center gap-2 border-b border-border p-2 md:hidden']) }}
>
    <x-ui.sheet>
        <x-ui.sheet.trigger
            :aria-label="__('Open navigation')"
            aria-controls="{{ $id }}"
            class="size-9 rounded-md text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring focus-visible:ring-offset-[length:var(--ring-offset-width)] focus-visible:ring-offset-background"
        >
            <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
        </x-ui.sheet.trigger>
        <x-ui.sheet.content id="{{ $id }}" side="start" class="flex flex-col overflow-y-auto">
            <x-ui.sheet.header>
                <x-ui.sheet.title>
                    @if ($hasBrand)
                        <span data-slot="sidebar-mobile-brand" @if (filled($label)) aria-hidden="true" @endif class="flex min-w-0 items-center gap-2">{{ $brand }}</span>
                        @if (filled($label))
                            <span class="sr-only">{{ __($label) }}</span>
                        @endif
                    @else
                        {{ __($label) }}
                    @endif
                </x-ui.sheet.title>
            </x-ui.sheet.header>
            <x-ui.sheet.close />
            @if (trim($slot) !== '')
                {{ $slot }}
            @else
                <nav aria-label="{{ __('Primary') }}" class="mt-4 flex flex-col gap-4">
                    @foreach ($nav as $group)
                        <div>
                            <p class="mb-2 px-2 text-xs font-medium text-muted-foreground">{{ __($group['heading']) }}</p>
                            <ul class="flex flex-col gap-0.5">
                                @foreach ($group['items'] as $item)
                                    <li>
                                        <a
                                            href="{{ $item['href'] }}"
                                            @if ($item['current'] ?? false) aria-current="page" @endif
                                            @class([
                                                'flex items-center gap-2 rounded-md px-2 py-2 text-sm font-medium transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring',
                                                'bg-muted text-foreground' => $item['current'] ?? false,
                                                'text-muted-foreground' => ! ($item['current'] ?? false),
                                            ])
                                        >
                                            @isset($item['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">{!! $item['icon'] !!}</svg>
                                            @endisset
                                            <span class="truncate">{{ __($item['label']) }}</span>
                                            @isset($item['badge'])
                                                <x-ui.badge variant="secondary" class="ms-auto">{{ $item['badge'] }}</x-ui.badge>
                                            @endisset
                                            <x-ui.sidebar.menu-count :count="$item['count'] ?? null" :count-expr="$item['countExpr'] ?? null" :count-label="$item['countLabel'] ?? null" />
                                        </a>
                                    </li>
                                @endforeach
                            </ul>
                        </div>
                    @endforeach
                </nav>
            @endif
            @if ($hasFooter)
                <div data-slot="sidebar-mobile-footer" class="mt-auto flex flex-col gap-2 border-t border-border pt-4">
                    {{ $footer }}
                </div>
            @endif
        </x-ui.sheet.content>
    </x-ui.sheet>
    @unless ($bare)
        @if ($hasBrand)
            <div data-slot="sidebar-mobile-brand" class="flex min-w-0 items-center gap-2">{{ $brand }}</div>
        @else
            <span class="truncate text-sm font-semibold">{{ __($label) }}</span>
        @endif
    @endunless
</div>
public/vendor/brok/sidebar-group-boot.js JS
/**
 * Sidebar group boot: applies a remembered open or closed state to a
 * collapsible `sidebar.group` while the page parses, so a folded group never
 * flashes open before Alpine starts.
 *
 * A classic, blocking script (no module, no Alpine) that `sidebar.group` with a
 * `storage-key` renders as its first child, from
 * `public/vendor/brok/sidebar-group-boot.js`. It is a file, not an inline
 * script, so it runs under a strict Content-Security-Policy (`script-src
 * 'self'`, no nonce, no 'unsafe-inline'). The group's rows come after it in the
 * document, so they are parsed with the right `data-open` already set.
 *
 * The rule is the same as `uiSidebarGroup` in sidebar.js (the two files cannot
 * import each other, and SidebarGroupStateTest fails when they drift):
 *   - the group's `data-storage-key` names the localStorage key;
 *   - 'open' or 'true' opens the group, 'closed' or 'false' folds it;
 *   - a missing or unknown value keeps the server-rendered `data-open`.
 *
 * localStorage access is wrapped in try/catch, so blocked storage (private
 * mode, file://) keeps the server-rendered state instead of throwing.
 *
 * Note: this file is plain JS served by the registry — no Blade syntax allowed.
 */
(function () {
    const script = document.currentScript;
    const group = script ? script.parentElement : null;
    const key = group ? group.getAttribute('data-storage-key') : null;

    if (!key) {
        return;
    }

    let value = null;

    try {
        value = window.localStorage.getItem(key);
    } catch (e) {
        value = null;
    }

    if (value === 'open' || value === 'true') {
        group.setAttribute('data-open', 'true');
    } else if (value === 'closed' || value === 'false') {
        group.setAttribute('data-open', 'false');
    }
})();

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.13.0
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0