Skip to main content

Navigation

Tabs code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/tabs.blade.php

@props([
    'tabs' => [],
    'default' => null,
    'variant' => 'underline',
    'vertical' => false,
    'icons' => [],
    'compact' => false,
    'syncHash' => false,
])

@php
    /*
    | Tabbed interface driven by Alpine. Pass `tabs` as [key => label] and provide
    | matching panels via named slots (slot name === key).
    |
    | variant: underline, pills
    | vertical: stack the tab list beside the panels instead of above them
    | icons: optional [key => icon name] map
    | compact: use a tighter horizontal tab treatment
    |
    | Usage:
    |   <x-ui.tabs :tabs="['overview' => 'Overview', 'activity' => 'Activity']">
    |       <x-slot:overview>...</x-slot:overview>
    |       <x-slot:activity>...</x-slot:activity>
    |   </x-ui.tabs>
    |   <x-ui.tabs variant="pills" vertical :tabs="[...]">...</x-ui.tabs>
    */
    $first = $default ?? array_key_first($tabs);
    $tabKeys = array_map(fn ($key) => (string) $key, array_keys($tabs));
    $defaultIndex = array_search((string) $first, $tabKeys, true);
    $defaultIndex = $defaultIndex === false ? 0 : $defaultIndex;
    $instanceId = substr(md5(json_encode($tabKeys).$variant.($vertical ? 'v' : '')), 0, 10);
    $isCompact = $compact && ! $vertical;
    $pillsListClasses = $vertical
        ? 'flex w-48 shrink-0 flex-col items-stretch gap-1 self-start rounded-xl bg-gray-100 p-1 dark:bg-gray-800'
        : ($isCompact
            ? 'inline-flex items-center gap-0.5 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-800'
            : 'inline-flex items-center gap-1 rounded-xl bg-gray-100 p-1 dark:bg-gray-800');
    $underlineListClasses = $vertical
        ? 'flex w-48 shrink-0 flex-col items-stretch gap-1 self-start border-e border-gray-200 pe-4 dark:border-gray-800'
        : 'flex items-center '.($isCompact ? 'gap-4' : 'gap-6').' border-b border-gray-200 dark:border-gray-800';
@endphp

<div
    x-data="{
        active: @js((string) $first),
        keys: @js($tabKeys),
        focusIndex: @js($defaultIndex),
        activate(key) {
            const index = this.keys.indexOf(key);

            if (index === -1) {
                return;
            }

            this.active = key;
            this.focusIndex = index;
        },
        focusKey(key) {
            this.$nextTick(() => this.$refs['tab-' + key]?.focus());
        },
        move(direction) {
            if (this.keys.length === 0) {
                return;
            }

            const next = (this.focusIndex + direction + this.keys.length) % this.keys.length;
            const key = this.keys[next];

            this.activate(key);
            this.focusKey(key);
        },
        moveTo(index) {
            if (this.keys.length === 0) {
                return;
            }

            const next = index < 0 ? this.keys.length - 1 : (index >= this.keys.length ? 0 : index);
            const key = this.keys[next];

            this.activate(key);
            this.focusKey(key);
        },
    }"
    @if ($syncHash)
        x-init="
            const selectFromHash = () => {
                const key = window.location.hash.slice(1);

                if (keys.includes(key)) {
                    activate(key);
                }
            };

            selectFromHash();
            window.addEventListener('hashchange', selectFromHash);
        "
    @endif
    {{ $attributes->merge(['class' => 'w-full'.($vertical ? ' flex gap-6' : '')]) }}
>
    @if ($variant === 'pills')
        <div class="{{ $pillsListClasses }}" role="tablist" @if ($vertical) aria-orientation="vertical" @endif>
            @foreach ($tabs as $key => $label)
                @php
                    $tabIcon = $icons[$key] ?? null;
                    $tabSlug = Str::slug((string) $key) ?: 'tab';
                    $tabId = "tabs-{$instanceId}-tab-{$tabSlug}";
                    $panelId = "tabs-{$instanceId}-panel-{$tabSlug}";
                @endphp
                <button
                    id="{{ $tabId }}"
                    x-ref="tab-{{ $key }}"
                    type="button"
                    role="tab"
                    aria-controls="{{ $panelId }}"
                    :aria-selected="(active === @js((string) $key)).toString()"
                    :tabindex="active === @js((string) $key) ? '0' : '-1'"
                    @click="activate(@js((string) $key)); @if ($syncHash) window.history.replaceState(null, '', '#{{ $key }}'); @endif"
                    @keydown.arrow-right.prevent="move(1)"
                    @keydown.arrow-down.prevent="move(1)"
                    @keydown.arrow-left.prevent="move(-1)"
                    @keydown.arrow-up.prevent="move(-1)"
                    @keydown.home.prevent="moveTo(0)"
                    @keydown.end.prevent="moveTo(keys.length - 1)"
                    :class="active === '{{ $key }}' ? 'bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'"
                    class="inline-flex items-center gap-1.5 font-medium transition cursor-pointer {{ $isCompact ? 'rounded-md px-2.5 py-1.5 text-xs' : 'rounded-lg px-3.5 py-1.5 text-sm' }} {{ $vertical ? 'text-start' : '' }}"
                >
                    @if ($tabIcon)
                        <x-ui.icon :name="$tabIcon" size="sm" />
                    @endif
                    <span>{{ $label }}</span>
                </button>
            @endforeach
        </div>
    @else
        <div class="{{ $underlineListClasses }}" role="tablist" @if ($vertical) aria-orientation="vertical" @endif>
            @foreach ($tabs as $key => $label)
                @php
                    $tabIcon = $icons[$key] ?? null;
                    $tabSlug = Str::slug((string) $key) ?: 'tab';
                    $tabId = "tabs-{$instanceId}-tab-{$tabSlug}";
                    $panelId = "tabs-{$instanceId}-panel-{$tabSlug}";
                @endphp
                <button
                    id="{{ $tabId }}"
                    x-ref="tab-{{ $key }}"
                    type="button"
                    role="tab"
                    aria-controls="{{ $panelId }}"
                    :aria-selected="(active === @js((string) $key)).toString()"
                    :tabindex="active === @js((string) $key) ? '0' : '-1'"
                    @click="activate(@js((string) $key)); @if ($syncHash) window.history.replaceState(null, '', '#{{ $key }}'); @endif"
                    @keydown.arrow-right.prevent="move(1)"
                    @keydown.arrow-down.prevent="move(1)"
                    @keydown.arrow-left.prevent="move(-1)"
                    @keydown.arrow-up.prevent="move(-1)"
                    @keydown.home.prevent="moveTo(0)"
                    @keydown.end.prevent="moveTo(keys.length - 1)"
                    :class="active === '{{ $key }}' ? 'border-brand-500 text-gray-900 dark:text-white' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-700'"
                    @if ($vertical)
                        class="-me-px inline-flex items-center gap-1.5 border-e-2 px-3 py-2 text-start text-sm font-medium transition cursor-pointer"
                    @else
                        class="-mb-px inline-flex items-center gap-1.5 border-b-2 px-1 {{ $isCompact ? 'py-2 text-xs' : 'py-3 text-sm' }} font-medium transition cursor-pointer"
                    @endif
                >
                    @if ($tabIcon)
                        <x-ui.icon :name="$tabIcon" size="sm" />
                    @endif
                    <span>{{ $label }}</span>
                </button>
            @endforeach
        </div>
    @endif

    <div class="{{ $vertical ? 'min-w-0 flex-1' : 'mt-5' }}">
        @foreach ($tabs as $key => $label)
            @php
                $tabSlug = Str::slug((string) $key) ?: 'tab';
                $tabId = "tabs-{$instanceId}-tab-{$tabSlug}";
                $panelId = "tabs-{$instanceId}-panel-{$tabSlug}";
            @endphp
            <div
                id="{{ $panelId }}"
                x-show="active === '{{ $key }}'"
                x-cloak
                role="tabpanel"
                aria-labelledby="{{ $tabId }}"
                tabindex="0"
            >
                {{ ${$key} ?? '' }}
            </div>
        @endforeach
    </div>
</div>