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
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.
Hide sections you don't need:
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:
<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.
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.
Size Variants
With Accessible Label
Pass a hidden accessible name via the default slot:
<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:
<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:
<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:
@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.
Accepted
The outline clears and nothing is said.
Refused
Your color stays, and you are told it did not save — the previous one is a click away in recents.
Slow to answer
The outline stays until the answer arrives.
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. |
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-withinring 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 withwire: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:
'components' => [
'color-picker' => ['size' => 'md'],
],