---
title: Color Picker
description: Native color picker with preview swatch
visibility: guest
draft: false
---

# Color Picker

The `<x-wirekit::color-picker>` component wraps the native `<input type="color">` in a circular swatch with a live hex value readout. The browser provides its OS-level color dialog on click.

## Usage

:::preview{title="Basic Color Picker"}
<x-wirekit::color-picker name="brand" value="#ab35ff" />
:::

By default the component wraps the native `<input type="color">` — the OS color dialog, byte-for-byte the same as previous versions.

## Popover Mode

Add `popover` for a custom, design-system-consistent picker that opens in a panel: a saturation/value plane, a hue slider, an alpha slider, a format-aware text field (cycle **HEX → RGB → HSL → OKLCH**), a screen eyedropper (where supported), copy-to-clipboard, your preset swatches, and a recent-colors history (localStorage). It's a from-scratch HSV picker — no third-party library. The outer prop surface (`name` / `value` / `disabled` / form binding) is identical to native mode, so flipping `popover` on never breaks form submission.

The **OKLCH** format is WireKit's own color space — every `--color-wk-*` design token is an `oklch()` value — so set `format="oklch"` (or cycle to it) when you want output you can paste straight into a theme.

:::preview{title="Popover picker with alpha + presets"}
<x-wirekit::color-picker
    name="accent"
    popover
    value="#3b82f6"
    :presets="['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#a855f7']"
/>
:::

Hide sections you don't need:

:::preview{title="Compact popover — no alpha, no recents"}
<x-wirekit::color-picker name="brandbg" popover value="#22c55e" :with-alpha="false" :with-recents="false" />
:::

### Native picker on touch devices

On phones and tablets you may prefer the platform's own color dialog — the same sheet the default (non-popover) variant opens — over the custom panel. Opt in with `native-on-mobile`: on touch-primary devices (CSS `pointer: coarse`) the swatch then opens the native dialog, while desktop pointers keep the full popover:

```blade
<x-wirekit::color-picker name="accent" popover native-on-mobile value="#3b82f6" />
```

The native sheet picks opaque sRGB hex — the alpha slider, presets, format cycling, and eyedropper remain popover-only conveniences, so leave the flag off when those matter on mobile too.

### Clear to "no color"

Add `with-clear` for a clear button in the panel that empties the bound value — useful when "no color selected" is a valid state in your form. Clearing sets the form field to an empty string and the swatch shows a "No color" state until the next pick. Because the native `<input type="color">` can't represent an empty value, the clear button is **popover only**.

:::preview{title="Popover picker with a clear button"}
<x-wirekit::color-picker name="background" popover with-clear value="#3b82f6" />
:::

### Custom trigger

In popover mode a `trigger` slot replaces the default swatch with your own control — a labeled pill, a larger swatch, an icon. WireKit keeps the open toggle, the positioning anchor, and the `aria-haspopup="dialog"` wiring; the slot supplies the visible content, and its text/icon becomes the trigger's accessible name (so label an icon-only trigger yourself). Provide non-interactive content (text, icon, a styled `span`) — it renders inside WireKit's trigger button.

:::preview{title="Custom trigger"}
<x-wirekit::color-picker name="theme-color" popover value="#22c55e">
    <x-slot:trigger>
        <span style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; border-radius: var(--radius-wk-md); border: 1px solid var(--color-wk-border); color: var(--color-wk-text); font-size: var(--text-wk-sm);">Choose color…</span>
    </x-slot:trigger>
</x-wirekit::color-picker>
:::

## Size Variants

:::preview{title="Three Sizes"}
<x-wirekit::row wrap>
    <x-wirekit::color-picker name="c1" value="#ef4444" size="sm" />
    <x-wirekit::color-picker name="c2" value="#10b981" size="md" />
    <x-wirekit::color-picker name="c3" value="#3b82f6" size="lg" />
</x-wirekit::row>
:::

## With Accessible Label

Pass a hidden accessible name via the default slot:

```blade
<x-wirekit::color-picker name="brand" value="#ab35ff">
    Brand color
</x-wirekit::color-picker>
```

The slot content is rendered with `sr-only` so sighted users see only the swatch, but assistive tech reads the label.

## Livewire Integration

`<x-wirekit::color-picker>` keeps its current color in internal state seeded from
the `value` prop, so when binding with Livewire pass the bound property as
`:value` **alongside** `wire:model` to seed the initial color:

```blade
<x-wirekit::color-picker wire:model.live="brandColor" :value="$brandColor" />
```

Without `:value` the picker starts at `#000000` until the user picks a color;
`wire:model` keeps it in sync afterward. This is the framework-agnostic seeding
pattern WireKit's stateful controls share — it works in plain Blade forms too.

## Optimistic UI

Pass the name of the Livewire method the picker should call, and the color is sent the moment it is settled:

```blade
<x-wirekit::color-picker
    name="brand"
    popover
    :value="$brand"
    optimistic="saveBrandColor"
/>
```

Load `wirekit-optimistic.js` alongside whichever bundle you already use — below it, in your layout:

```blade
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}"></script>
```

### Try it

The demo below runs the real path: the change shows immediately, the outline says it is
provisional, and the server's answer either confirms it silently or takes it back.

:::preview{title="Optimistic color — one picker per answer the server can give"}
<livewire:demos.optimistic-host>
<x-wirekit::row gap="lg" style="flex-wrap: wrap;">
    <x-wirekit::stack gap="xs">
        <x-wirekit::text size="sm" weight="medium">Accepted</x-wirekit::text>
        <x-wirekit::color-picker name="opt-accept" popover value="#3b6ea5" optimistic="demoAccept" />
        <x-wirekit::text size="xs" variant="muted">The outline clears and nothing is said.</x-wirekit::text>
    </x-wirekit::stack>
    <x-wirekit::stack gap="xs">
        <x-wirekit::text size="sm" weight="medium">Refused</x-wirekit::text>
        <x-wirekit::color-picker name="opt-reject" popover value="#3b6ea5" optimistic="demoReject" />
        <x-wirekit::text size="xs" variant="muted">Your color stays, and you are told it did not save — the previous one is a click away in recents.</x-wirekit::text>
    </x-wirekit::stack>
    <x-wirekit::stack gap="xs">
        <x-wirekit::text size="sm" weight="medium">Slow to answer</x-wirekit::text>
        <x-wirekit::color-picker name="opt-slow" popover value="#3b6ea5" optimistic="demoSlow" />
        <x-wirekit::text size="xs" variant="muted">The outline stays until the answer arrives.</x-wirekit::text>
    </x-wirekit::stack>
</x-wirekit::row>
</livewire:demos.optimistic-host>
:::

:::source{language="blade"}
{{-- In your app there is no host: your own Livewire component owns the method. --}}
<x-wirekit::color-picker
    name="brand"
    popover
    :value="$brand"
    optimistic="saveBrandColor"
/>
:::

The `<livewire:demos.…>` wrapper above exists only on this site — it supplies the demo
methods so the page can show a real round trip. The block under it is what you write.

**"Settled" means the gesture finished, never a timer.** Dragging across the saturation plane sends nothing while the pointer is down; the color goes out when you release it. A swatch, a recent, the eyedropper, an arrow-key nudge and a typed value each settle in one step and send immediately.

**A refusal keeps the color rather than undoing it.** The value field is typed, and undoing a failed save would delete what you wrote. The refused color therefore stays on screen and says it was not saved — which is recoverable, because the previous color is one click away in the recents strip.

The field is **not** marked invalid. `aria-invalid` means *this value is wrong*, and a save that failed on the network says nothing about the color you picked.

Popover mode only (`popover`). The default renders a native `<input type="color">`, whose value the browser owns — there is no seam there to hook.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string\|null` | `null` | Form field name |
| `id` | `string\|null` | auto-generated | Element id |
| `value` | `string` | `'#000000'` | Initial hex color |
| `size` | `string` | `'md'` | `'sm'`, `'md'`, `'lg'` |
| `showValue` | `bool` | `true` | Show live value next to swatch |
| `disabled` | `bool` | `false` | Disabled state |
| `popover` | `bool` | `false` | Use the custom popover picker instead of the native dialog |
| `nativeOnMobile` | `bool` | `false` | Popover only — open the OS-native color dialog on touch-primary devices (`pointer: coarse`) |
| `format` | `string` | `'hex'` | Popover default format — `hex`, `rgb`, `hsl`, or `oklch` |
| `withAlpha` | `bool` | `true` | Show the alpha slider (popover only) |
| `withEyedropper` | `bool` | `true` | Show the screen eyedropper (where the browser supports it) |
| `withRecents` | `bool` | `true` | Show the recent-colors history (localStorage) |
| `withClear` | `bool` | `false` | Popover only — show a clear button that empties the bound value ("no color") |
| `presets` | `array` | `[]` | Preset swatch colors (popover only) |
| `recentsKey` | `string\|null` | `null` | localStorage key for recents (default `wk-color-picker-recents`) |
| `scope` | `string\|null` | `null` | Scoped personalization key |
| `optimistic` | `string\|null` | `null` | Livewire method to call once a color is settled, showing it before the server answers. A refusal **keeps the color** and says it was not saved. Popover mode only. See [Optimistic UI](#optimistic-ui). |
| `optimisticArgs` | `array` | `[]` | Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to. |

## Accessibility

- Uses native `<input type="color">` — full keyboard + AT support
- `<label>` wraps the swatch, allowing click-to-open on the visible circle
- Live value display uses `aria-live="polite"` for AT announcements on change
- `focus-within` ring on the swatch indicates keyboard focus

## Keyboard Interaction

| Key | Action |
|-----|--------|
| `Tab` | Move focus to the color picker control |
| `Enter` / `Space` | Open the system color dialog |
| Type a `#hexcode` value | Set the value directly via the text-input fallback |

## Pitfalls

- **Don't rely on the native color picker for brand-locked palettes.** It exposes the entire RGB gamut. For constrained palettes, render `<x-wirekit::badge>` swatches with `wire:click`.
- **Don't store the value as `rgba()`.** The component returns `#rrggbb` (sRGB hex). Store hex; convert to other spaces at render time.

## Design Tokens

| Token | Used for |
| --- | --- |
| `--font-wk-mono` | Live hex value font |
| `--text-wk-sm` | Live hex value font size |
| `--color-wk-text` | Hex value text |
| `--color-wk-border` | Swatch border |
| `--color-wk-ring` | Focus ring |
| `--ring-wk-width` | Focus ring width |
| `--border-wk-width` | Swatch border width |
| `--padding-wk-x-sm` | Hex pill padding |
| `--transition-wk-duration` | Focus / hover transition |

## Config Defaults

The defaults live in `config/wirekit.php` under `components.color-picker`. Override them globally:

```php
'components' => [
    'color-picker' => ['size' => 'md'],
],
```

## Further Reading

- [MDN: `<input type="color">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color)
- [MDN: Hex color notation](https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color)
- [MDN: `aria-live`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-live)
- [Can I Use: `input type=color`](https://caniuse.com/input-color)
