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
Default (native) mode:
- 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
Popover mode:
- The swatch is a
<button>witharia-haspopup="dialog"and a livearia-expanded - The panel is a
role="dialog"with an accessible name, and focus is trapped inside it while it is open — opening the panel moves focus onto the saturation/brightness plane, and closing it returns focus to the swatch that opened it - The panel renders at the end of the document (it is positioned relative to the swatch rather than nested inside it), so the focus handling above is what keeps it reachable — a reader never tabs through the rest of the page to get to the sliders
- The plane and the hue/opacity sliders are
role="slider"elements witharia-valuetext/aria-valuenow, so the current color is announced as it changes - Copying and clearing announce themselves through their own polite live regions
Keyboard Interaction
Default (native) mode:
| 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 |
Popover mode:
| Key | Action |
|---|---|
Tab |
Move focus to the swatch; once the panel is open, cycle between the controls inside it |
Enter / Space |
Open the panel from the swatch — focus lands on the saturation/brightness plane |
Arrow Left / Arrow Right |
On the plane: adjust saturation. On the hue or opacity slider: adjust that value |
Arrow Up / Arrow Down |
On the plane: adjust brightness |
Escape |
Close the panel and return focus to the swatch |
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'],
],