Skip to main content

Actions

Split Button code

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

resources/views/components/ui/split-button.blade.php

@props([
    'variant' => 'primary',
    'size' => 'base',
    'icon' => null,
    'iconWeight' => 'bold',
    'href' => null,
    'type' => 'submit',
    'align' => 'right',
    'width' => '48',
    'loading' => false,
])

@php
    /*
    | Button with a primary action plus a dropdown of related actions. The
    | label goes in the default slot (renders an <a> when `href` is set) and
    | <x-ui.dropdown.item> rows go in the `menu` slot.
    |
    | Variants: primary, secondary, outline, ghost, danger, success, accent
    | Sizes:    xs, sm, base, lg, xl
    |
    | Usage:
    |   <x-ui.split-button icon="plus">
    |       Create project
    |       <x-slot:menu>
    |           <x-ui.dropdown.item icon="copy">Duplicate existing</x-ui.dropdown.item>
    |           <x-ui.dropdown.item icon="download-simple">Import from CSV</x-ui.dropdown.item>
    |       </x-slot:menu>
    |   </x-ui.split-button>
    */
    $base = 'inline-flex items-center justify-center gap-2 font-semibold transition-[box-shadow,border-color,color,transform] ease-in-out duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:opacity-50 disabled:pointer-events-none cursor-pointer whitespace-nowrap';

    $variants = [
        'primary' => 'bg-brand-600 text-white shadow-sm hover:bg-brand-500 active:bg-brand-700 focus:ring-brand-500',
        'secondary' => 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-700 focus:ring-gray-400',
        'outline' => 'bg-transparent border border-brand-300 text-brand-700 hover:bg-brand-50 focus:ring-brand-500 dark:border-brand-700 dark:text-brand-300 dark:hover:bg-brand-950/40',
        'ghost' => 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:ring-gray-400',
        'danger' => 'bg-red-600 text-white shadow-sm hover:bg-red-500 active:bg-red-700 focus:ring-red-500',
        'success' => 'bg-green-600 text-white shadow-sm hover:bg-green-500 active:bg-green-700 focus:ring-green-500',
        'accent' => 'bg-accent-600 text-white shadow-sm hover:bg-accent-500 active:bg-accent-700 focus:ring-accent-500',
    ];

    $sizes = [
        'xs' => 'text-xs px-2.5 py-1.5',
        'sm' => 'text-sm px-3 py-2',
        'base' => 'text-sm px-4 py-2.5',
        'lg' => 'text-base px-5 py-3',
        'xl' => 'text-base px-6 py-3.5',
    ];

    $toggleSizes = [
        'xs' => 'px-1.5', 'sm' => 'px-2', 'base' => 'px-2.5', 'lg' => 'px-3', 'xl' => 'px-3.5',
    ];

    $iconSize = [
        'xs' => 'sm', 'sm' => 'sm', 'base' => 'base', 'lg' => 'lg', 'xl' => 'lg',
    ][$size] ?? 'base';

    $divider = [
        'primary' => 'border-brand-500 dark:border-brand-700',
        'secondary' => 'border-gray-200 dark:border-gray-700',
        'outline' => 'border-brand-300 dark:border-brand-700',
        'ghost' => 'border-gray-200 dark:border-gray-800',
        'danger' => 'border-red-500 dark:border-red-700',
        'success' => 'border-green-500 dark:border-green-700',
        'accent' => 'border-accent-500 dark:border-accent-700',
    ];

    $variantClass = $variants[$variant] ?? $variants['primary'];
    $sizeClass = $sizes[$size] ?? $sizes['base'];
    $toggleSizeClass = $toggleSizes[$size] ?? $toggleSizes['base'];
    $dividerClass = $divider[$variant] ?? $divider['primary'];

    $alignClass = $align === 'left' ? 'origin-top-left start-0' : 'origin-top-right end-0';
    $widthClass = ['48' => 'w-48', '56' => 'w-56', '64' => 'w-64'][$width] ?? $width;

    $menuId = 'split-button-'.substr(md5($variant.$size.$slot), 0, 10);
@endphp

<div
    x-data="{ open: false }"
    @click.outside="open = false"
    @keydown.escape.window="open = false"
    {{ $attributes->merge(['class' => 'relative inline-flex']) }}
>
    @if ($href)
        <a href="{{ $href }}" class="{{ $base }} {{ $variantClass }} {{ $sizeClass }} rounded-s-lg rounded-e-none">
            @if ($loading)
                <x-ui.icon name="spinner-gap" :size="$iconSize" class="animate-spin" />
            @elseif ($icon)
                <x-ui.icon :name="$icon" :weight="$iconWeight" :size="$iconSize" />
            @endif
            {{ $slot }}
        </a>
    @else
        <button type="{{ $type }}" @disabled($loading) class="{{ $base }} {{ $variantClass }} {{ $sizeClass }} rounded-s-lg rounded-e-none">
            @if ($loading)
                <x-ui.icon name="spinner-gap" :size="$iconSize" class="animate-spin" />
            @elseif ($icon)
                <x-ui.icon :name="$icon" :weight="$iconWeight" :size="$iconSize" />
            @endif
            {{ $slot }}
        </button>
    @endif

    <button
        type="button"
        @click="open = ! open"
        @keydown.arrow-down.prevent="open = true"
        :aria-expanded="open.toString()"
        aria-controls="{{ $menuId }}"
        aria-haspopup="menu"
        aria-label="More actions"
        class="{{ $base }} {{ $variantClass }} {{ $toggleSizeClass }} rounded-e-lg rounded-s-none border-s {{ $dividerClass }}"
    >
        <x-ui.icon name="caret-down" weight="bold" :size="$iconSize" x-bind:class="open && 'rotate-180'" class="transition-transform" />
    </button>

    <div
        id="{{ $menuId }}"
        x-show="open" x-cloak
        x-transition:enter="transition ease-out duration-150"
        x-transition:enter-start="opacity-0 scale-95"
        x-transition:enter-end="opacity-100 scale-100"
        x-transition:leave="transition ease-in duration-100"
        x-transition:leave-start="opacity-100 scale-100"
        x-transition:leave-end="opacity-0 scale-95"
        class="absolute top-full z-50 mt-2 {{ $widthClass }} {{ $alignClass }} rounded-xl border border-gray-200 bg-white p-1.5 shadow-lg ring-1 ring-black/5 dark:border-gray-800 dark:bg-gray-900"
        role="menu"
        @click="open = false"
    >
        {{ $menu }}
    </div>
</div>