Color Picker
The <x-wirekit:: 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 aria-label="Accent color" 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:: 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 aria-label="Brand color" 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.
The binding works the same with popover: wire:model and x-model go to the field that carries the picked value, and every other attribute you write on the tag, such as a data-* hook or an x-on:change listener, goes to the element around the picker, where the field's input and change events reach it.
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
aria-label="Brand color"
name="brand"
popover
:value="$brand"
optimistic="saveBrandColor"
/>
Load wirekit-optimistic. alongside whichever bundle you already use — below it, in your layout:
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}" defer></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.
This color falls below 4.5:1 against your background.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
required |
bool | false |
Marks the field as required. Emits aria-required="true" on the color input. The native attribute is not used: HTML ignores required on type="color", which always has a value. No visible indicator is rendered — this component's accessible name is its default slot, rendered sr-only, so there is no visible label to mark. In popover mode the field is a trigger button, which takes no required state, so a required picker cannot be emptied: withClear renders no button there. |
name |
string|null |
null |
Form field name |
id |
string|null |
auto-generated | The id of the field, or of the trigger with popover. A <label for> of yours with this id names the field, and then no fallback name stands over it |
value |
string |
'#000000' |
Initial hex color |
size |
string |
'md' |
'sm', 'md', 'lg' |
showValue |
bool |
true |
Show live value next to swatch |
disabled |
bool |
false |
Disables the picker. Like a disabled native field, it is left out of the data a form submits |
hint |
string|null |
null |
Helper text below the swatch |
error |
string|null |
null |
Error message (also reads from $errors, keyed by name). Replaces the hint, paints the swatch ring in the error color, and sets aria-invalid |
announceError |
bool|null | null |
Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). Set false when the page runs its own live region. |
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"). A required picker renders none, since it has no empty state |
presets |
array |
[] |
Preset swatch colors (popover only), each a hex color, rgb(), hsl() or oklch(). An entry in any other form, a color name included, draws no swatch |
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 focuserrorandhintrender one message region below the swatch (the error replaces the hint), and the control points at whichever one is on the page witharia-describedby— an error also setsaria-invalid="true"
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
- A copy that fails, on a page served without HTTPS where browsers offer no clipboard, or when the browser refuses the permission, says so: the button shows a warning instead of the checkmark, and an alert says "Copy failed"
- A value the text field cannot read marks the field invalid and says why under the row, with an example the field reads: the last color it read, in the format the toggle shows, "Not a valid color value. Example: #3b82f6". The same sentence describes the field for a screen reader
- The copy and clear buttons are named after the picker, by the name its swatch carries: the
aria-label, the label of the field around it, or the name built fromname. A picker withname="background"offers "Copy Background color" and "Clear Background color", and one with none of them "Copy color value" and "Clear color"
Keyboard Interaction
Default (native) mode:
| Key | Action |
|---|---|
Tab |
Move focus to the color picker control |
Enter / Space |
Open the system color dialog |
Native mode has no text field: the only text input this component renders is in popover
mode, so a reader who cannot use the OS color dialog has no way to type a value there. Use
popover when a typed value has to be possible.
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 |
Home / End |
Jump to the ends of the range. On the plane this moves saturation and leaves brightness alone — a plane has two ranges and one Home key, and saturation is the axis the left and right arrows already drive |
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 a constrained palette, render each swatch as a real control:
<x-wirekit::gives the whole set one tab stop and arrow-key selection, which is what a pick-one-of-N palette is;radio name="brand-color" value="#0f766e"> <x-wirekit::works where a single click is enough. A presentational component — anything whose root is abutton> <span>— carries a click handler for the mouse only: it is not in the tab order, and Enter and Space do nothing. - 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-border-error |
Swatch border while the field is in error |
--color-wk-danger-text |
Error message text |
--color-wk-text-muted |
Hint text |
--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/ under components.. Override them globally:
'components' => [
'color-picker' => ['size' => 'md'],
],