# Sidebar

A composable, themeable and customizable sidebar component.

- Install: `php artisan blatui:add sidebar`
- Source: https://blatui.remix-it.com/r/sidebar.json
- Composer: `composer require mallardduck/blade-lucide-icons gehrisandro/tailwind-merge-laravel`
- npm: `npm install -D @alpinejs/focus`

## resources/views/components/ui/sidebar.blade.php

```blade
@props([
    'side' => 'left',
    'variant' => 'sidebar',
    'collapsible' => 'offcanvas',
])

@php
    $isLeft = $side === 'left';
@endphp

@if ($collapsible === 'none')
    {{-- Non-collapsible: a plain in-flow panel (no fixed pinning). --}}
    <div
        data-slot="sidebar"
        data-variant="{{ $variant }}"
        data-side="{{ $side }}"
        {{ $attributes->twMerge('bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col') }}
    >
        {{ $slot }}
    </div>
@else
{{-- Desktop --}}
<div
    class="text-sidebar-foreground group peer hidden md:block"
    :data-state="open ? 'expanded' : 'collapsed'"
    :data-collapsible="open ? '' : @js($collapsible)"
    data-variant="{{ $variant }}"
    data-side="{{ $side }}"
    data-slot="sidebar"
>
    {{-- gap --}}
    <div
        class="relative bg-transparent transition-[width] duration-200 ease-linear"
        :style="open
            ? 'width: var(--sidebar-width)'
            : (@js($collapsible) === 'icon' ? 'width: var(--sidebar-width-icon)' : 'width: 0')"
    ></div>
    {{-- fixed container --}}
    <div
        class="fixed inset-y-0 z-10 hidden h-svh transition-[left,right,width,transform] duration-200 ease-linear md:flex {{ $isLeft ? 'left-0' : 'right-0' }}"
        :style="open
            ? 'width: var(--sidebar-width)'
            : (@js($collapsible) === 'icon'
                ? 'width: var(--sidebar-width-icon)'
                : 'width: var(--sidebar-width); transform: translateX({{ $isLeft ? '-100%' : '100%' }})')"
    >
        <div
            data-sidebar="sidebar"
            class="bg-sidebar flex h-full w-full flex-col {{ $isLeft ? 'border-r' : 'border-l' }} border-sidebar-border"
        >
            {{ $slot }}
        </div>
    </div>
</div>

{{-- Mobile overlay --}}
<template x-teleport="body">
    <div x-show="openMobile" x-cloak class="fixed inset-0 z-50 md:hidden">
        <div
            x-show="openMobile"
            @click="openMobile = false"
            role="presentation"
            aria-hidden="true"
            x-transition:enter="transition ease-out duration-300"
            x-transition:enter-start="opacity-0"
            x-transition:enter-end="opacity-100"
            x-transition:leave="transition ease-in duration-200"
            x-transition:leave-start="opacity-100"
            x-transition:leave-end="opacity-0"
            class="fixed inset-0 bg-black/50"
        ></div>
        <div
            x-show="openMobile"
            x-trap.noscroll.inert="openMobile"
            @keydown.escape.window="openMobile = false"
            role="dialog"
            aria-modal="true"
            aria-label="Sidebar"
            tabindex="-1"
            x-transition:enter="transition ease-in-out duration-300"
            x-transition:enter-start="{{ $isLeft ? '-translate-x-full' : 'translate-x-full' }}"
            x-transition:enter-end="translate-x-0"
            x-transition:leave="transition ease-in-out duration-200"
            x-transition:leave-start="translate-x-0"
            x-transition:leave-end="{{ $isLeft ? '-translate-x-full' : 'translate-x-full' }}"
            class="bg-sidebar text-sidebar-foreground fixed inset-y-0 {{ $isLeft ? 'left-0' : 'right-0' }} z-50 flex h-svh w-(--sidebar-width) flex-col"
        >
            {{ $slot }}
        </div>
    </div>
</template>
@endif
```

## resources/views/components/ui/sidebar-content.blade.php

```blade
<div data-slot="sidebar-content" data-sidebar="content" {{ $attributes->twMerge('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden') }}>
    {{ $slot }}
</div>
```

## resources/views/components/ui/sidebar-footer.blade.php

```blade
<div data-slot="sidebar-footer" data-sidebar="footer" {{ $attributes->twMerge('flex flex-col gap-2 p-2') }}>
    {{ $slot }}
</div>
```

## resources/views/components/ui/sidebar-group.blade.php

```blade
<div data-slot="sidebar-group" data-sidebar="group" {{ $attributes->twMerge('relative flex w-full min-w-0 flex-col p-2') }}>
    {{ $slot }}
</div>
```

## resources/views/components/ui/sidebar-group-content.blade.php

```blade
<div data-slot="sidebar-group-content" data-sidebar="group-content" {{ $attributes->twMerge('w-full text-sm') }}>
    {{ $slot }}
</div>
```

## resources/views/components/ui/sidebar-group-label.blade.php

```blade
<div data-slot="sidebar-group-label" data-sidebar="group-label" {{ $attributes->twMerge("text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 [&>svg]:size-4 [&>svg]:shrink-0") }}>
    {{ $slot }}
</div>
```

## resources/views/components/ui/sidebar-header.blade.php

```blade
<div data-slot="sidebar-header" data-sidebar="header" {{ $attributes->twMerge('flex flex-col gap-2 p-2') }}>
    {{ $slot }}
</div>
```

## resources/views/components/ui/sidebar-input.blade.php

```blade
<input
    data-slot="sidebar-input"
    data-sidebar="input"
    {{ $attributes->twMerge('bg-background h-8 w-full rounded-md border border-input px-3 py-1 text-sm shadow-none outline-none placeholder:text-muted-foreground focus-visible:ring-ring/50 focus-visible:ring-[3px]') }}
>
```

## resources/views/components/ui/sidebar-inset.blade.php

```blade
<main data-slot="sidebar-inset" {{ $attributes->twMerge('bg-background relative flex w-full flex-1 flex-col md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm') }}>
    {{ $slot }}
</main>
```

## resources/views/components/ui/sidebar-menu.blade.php

```blade
<ul data-slot="sidebar-menu" data-sidebar="menu" {{ $attributes->twMerge('flex w-full min-w-0 flex-col gap-1') }}>
    {{ $slot }}
</ul>
```

## resources/views/components/ui/sidebar-menu-action.blade.php

```blade
@props(['showOnHover' => false])

<button
    type="button"
    data-slot="sidebar-menu-action"
    data-sidebar="menu-action"
    {{ $attributes->twMerge('text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 group-data-[collapsible=icon]:hidden [&>svg]:size-4 [&>svg]:shrink-0 '.($showOnHover ? 'md:opacity-0 group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100' : '')) }}
>
    {{ $slot }}
</button>
```

## resources/views/components/ui/sidebar-menu-badge.blade.php

```blade
<div data-slot="sidebar-menu-badge" data-sidebar="menu-badge" {{ $attributes->twMerge('text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground top-1.5 group-data-[collapsible=icon]:hidden') }}>
    {{ $slot }}
</div>
```

## resources/views/components/ui/sidebar-menu-button.blade.php

```blade
@props([
    'href' => null,
    'isActive' => false,
    'variant' => 'default',
    'size' => 'default',
])

@php
    $base = "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0";
    $variants = [
        'default' => 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
        'outline' => 'bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]',
    ];
    $sizes = [
        'default' => 'h-8 text-sm',
        'sm' => 'h-7 text-xs',
        'lg' => 'h-12 text-sm',
    ];
    // Emit exactly ONE collapsed-icon padding class so it can't conflict in twMerge:
    // lg buttons (logo/avatar fills the icon box) get p-0, the rest get p-2.
    $iconPad = $size === 'lg' ? 'group-data-[collapsible=icon]:p-0!' : 'group-data-[collapsible=icon]:p-2!';
    $classes = $base.' '.($variants[$variant] ?? $variants['default']).' '.($sizes[$size] ?? $sizes['default']).' '.$iconPad;
@endphp

@if ($href)
    <a
        href="{{ $href }}"
        data-slot="sidebar-menu-button"
        data-sidebar="menu-button"
        data-size="{{ $size }}"
        @if ($isActive) data-active="true" aria-current="page" @endif
        {{ $attributes->twMerge($classes) }}
    >{{ $slot }}</a>
@else
    <button
        type="button"
        data-slot="sidebar-menu-button"
        data-sidebar="menu-button"
        data-size="{{ $size }}"
        @if ($isActive) data-active="true" @endif
        {{ $attributes->twMerge($classes) }}
    >{{ $slot }}</button>
@endif
```

## resources/views/components/ui/sidebar-menu-item.blade.php

```blade
<li data-slot="sidebar-menu-item" data-sidebar="menu-item" {{ $attributes->twMerge('group/menu-item relative') }}>
    {{ $slot }}
</li>
```

## resources/views/components/ui/sidebar-menu-sub.blade.php

```blade
<ul data-slot="sidebar-menu-sub" data-sidebar="menu-sub" {{ $attributes->twMerge('border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden') }}>
    {{ $slot }}
</ul>
```

## resources/views/components/ui/sidebar-menu-sub-button.blade.php

```blade
@props([
    'href' => '#',
    'isActive' => false,
    'size' => 'md',
])

@php
    $classes = "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 group-data-[collapsible=icon]:hidden ".($size === 'sm' ? 'text-xs' : 'text-sm');
@endphp

<a
    href="{{ $href }}"
    data-slot="sidebar-menu-sub-button"
    data-sidebar="menu-sub-button"
    data-size="{{ $size }}"
    @if ($isActive) data-active="true" @endif
    {{ $attributes->twMerge($classes) }}
>{{ $slot }}</a>
```

## resources/views/components/ui/sidebar-menu-sub-item.blade.php

```blade
<li data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" {{ $attributes->twMerge('group/menu-sub-item relative') }}>
    {{ $slot }}
</li>
```

## resources/views/components/ui/sidebar-provider.blade.php

```blade
@props(['defaultOpen' => true])

@php
    // Merge our default CSS vars with any style passed by the block (e.g. a custom
    // --sidebar-width or --header-height). Passed declarations come last so they win,
    // and we drop the incoming `style` from the bag to avoid a duplicate attribute.
    $style = rtrim('--sidebar-width: 16rem; --sidebar-width-icon: 3rem; '.$attributes->get('style', ''));
@endphp

<div
    data-slot="sidebar-provider"
    x-data="{
        open: {{ $defaultOpen ? 'true' : 'false' }},
        openMobile: false,
        isMobile: false,
        toggle() { this.isMobile ? (this.openMobile = !this.openMobile) : (this.open = !this.open) },
        init() {
            const mq = window.matchMedia('(max-width: 767px)');
            this.isMobile = mq.matches;
            mq.addEventListener('change', e => this.isMobile = e.matches);
        }
    }"
    style="{{ $style }}"
    {{ $attributes->except('style')->twMerge('group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar') }}
>
    {{ $slot }}
</div>
```

## resources/views/components/ui/sidebar-rail.blade.php

```blade
<button
    type="button"
    @click="toggle()"
    data-slot="sidebar-rail"
    data-sidebar="rail"
    aria-label="Toggle Sidebar"
    title="Toggle sidebar"
    tabindex="-1"
    {{ $attributes->twMerge('hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 cursor-pointer transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-px sm:flex') }}
></button>
```

## resources/views/components/ui/sidebar-separator.blade.php

```blade
<div data-slot="sidebar-separator" data-sidebar="separator" {{ $attributes->twMerge('bg-sidebar-border mx-2 w-auto h-px') }}></div>
```

## resources/views/components/ui/sidebar-trigger.blade.php

```blade
<button
    type="button"
    @click="toggle()"
    data-slot="sidebar-trigger"
    aria-label="Toggle Sidebar"
    :aria-expanded="isMobile ? openMobile : open"
    {{ $attributes->twMerge('hover:bg-accent hover:text-accent-foreground inline-flex size-7 items-center justify-center rounded-md transition-colors outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]') }}
>
    <x-lucide-panel-left class="size-4" aria-hidden="true" />
    <span class="sr-only">Toggle Sidebar</span>
</button>
```
