Skip to main content

Forms

Color Picker code

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

resources/views/components/ui/color-picker.blade.php

@props([
    'name' => null,
    'value' => null,
    'swatches' => null,
    'size' => 'base',
    'invalid' => false,
    'disabled' => false,
])

@php
    /*
    | Color picker. A trigger showing the current swatch + hex opens a popover
    | with preset swatches, a native colour wheel and a hex text field. The
    | chosen value is submitted as a `#rrggbb` hex string via a hidden input
    | bound to `name`. Override the presets with the `swatches` array.
    |
    | Usage:
    |   <x-ui.color-picker name="brand_color" />
    |   <x-ui.color-picker name="accent" :swatches="['#ef4444', '#f59e0b', '#10b981', '#3b82f6']" />
    |   <x-ui.color-picker x-model="characterColor" :value="$characterColor" />
    */
    $value ??= \App\Support\ThemeTokens::brand('600');

    $sizes = [
        'sm' => 'text-sm py-1.5',
        'base' => 'text-sm py-2.5',
        'lg' => 'text-base py-3',
    ];

    $ring = $invalid
        ? 'border-red-400 dark:border-red-500 focus:border-red-500 focus:ring-red-500'
        : 'border-gray-300 dark:border-gray-700 focus:border-brand-500 focus:ring-brand-500';

    $classes = implode(' ', [
        'flex w-full items-center gap-2.5 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 shadow-sm ps-2.5 pe-3.5 text-left transition focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed',
        $sizes[$size] ?? $sizes['base'],
        $ring,
    ]);

    $defaultSwatches = [
        \App\Support\ThemeTokens::brand('600'),
        \App\Support\ThemeTokens::brand('500'),
        \App\Support\ThemeTokens::accent('600'),
        \App\Support\ThemeTokens::accent('500'),
        '#ef4444', '#f59e0b', '#16a34a', '#0ea5e9',
        '#6366f1', '#8b5cf6', '#ec4899', \App\Support\ThemeTokens::gray('500'),
    ];
    $palette = collect($swatches ?: $defaultSwatches)
        ->map(fn ($hex) => strtolower((string) $hex))
        ->values()
        ->all();
@endphp

<div
    x-data="{
        open: false,
        color: @js(strtolower((string) $value)),
        disabled: @js((bool) $disabled),
        swatches: @js($palette),
        setColor(hex) {
            const v = hex.toLowerCase();
            if (/^#[0-9a-f]{6}$/.test(v)) { this.color = v; }
        },
        onHexInput(event) {
            let v = event.target.value.trim().toLowerCase();
            if (v && ! v.startsWith('#')) { v = '#' + v; }
            if (/^#[0-9a-f]{6}$/.test(v)) { this.color = v; }
        },
        isActive(hex) { return this.color === hex.toLowerCase(); },
    }"
    x-modelable="color"
    @click.outside="open = false"
    @keydown.escape.window="open = false"
    {{ $attributes->class(['relative']) }}
>
    @if ($name)
        <input type="hidden" name="{{ $name }}" :value="color">
    @endif

    <button
        type="button"
        @disabled($disabled)
        @if ($invalid) aria-invalid="true" @endif
        @click="open = ! open"
        class="{{ $classes }}"
    >
        <span class="h-6 w-6 shrink-0 rounded-md ring-1 ring-inset ring-black/10 dark:ring-white/10" :style="`background-color: ${color}`"></span>
        <span class="flex-1 font-medium uppercase tabular-nums" x-text="color"></span>
        <x-ui.icon name="caret-down" weight="bold" size="sm" class="text-gray-400 dark:text-gray-500" ::class="open ? 'rotate-180 transition-transform' : 'transition-transform'" />
    </button>

    <div
        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"
        class="absolute z-50 mt-2 w-64 rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-3 shadow-lg ring-1 ring-black/5"
    >
        <div class="grid grid-cols-6 gap-2">
            <template x-for="hex in swatches" :key="hex">
                <button
                    type="button"
                    @click="setColor(hex)"
                    :style="`background-color: ${hex}`"
                    :class="isActive(hex) ? 'ring-2 ring-offset-2 ring-offset-white dark:ring-offset-gray-900 ring-brand-500' : 'ring-1 ring-inset ring-black/10 dark:ring-white/10'"
                    class="h-7 w-7 rounded-lg transition hover:scale-110 cursor-pointer"
                    :aria-label="hex"
                >
                    <x-ui.icon name="check" weight="bold" size="sm" class="text-white mix-blend-difference" x-show="isActive(hex)" />
                </button>
            </template>
        </div>

        <div class="mt-3 flex items-center gap-2 border-t border-gray-100 dark:border-gray-800 pt-3">
            <label class="relative h-9 w-9 shrink-0 cursor-pointer overflow-hidden rounded-lg ring-1 ring-inset ring-black/10 dark:ring-white/10" :style="`background-color: ${color}`">
                <input type="color" :value="color" @input="setColor($event.target.value)" class="absolute inset-0 h-full w-full cursor-pointer opacity-0">
            </label>

            <div class="relative flex-1">
                <span class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-2.5 text-gray-400 dark:text-gray-500">#</span>
                <input
                    type="text"
                    maxlength="7"
                    :value="color"
                    @input="onHexInput($event)"
                    spellcheck="false"
                    class="block w-full rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 py-1.5 ps-6 pe-2.5 text-sm uppercase tabular-nums text-gray-900 dark:text-gray-100 shadow-sm transition focus:border-brand-500 focus:ring-1 focus:ring-brand-500"
                >
            </div>
        </div>
    </div>
</div>