Skip to main content
Copy for LLM

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

Basic Color Picker

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.

Popover picker with alpha + presets

Hide sections you don't need:

Compact popover — no alpha, no recents

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.

Popover picker with a clear button

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.

Custom trigger

Size Variants

Three Sizes

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 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.js 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.

Optimistic color — one picker per answer the server can give

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.

Error state

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-within ring on the swatch indicates keyboard focus
  • error and hint render one message region below the swatch (the error replaces the hint), and the control points at whichever one is on the page with aria-describedby — an error also sets aria-invalid="true"

Popover mode:

  • The swatch is a <button> with aria-haspopup="dialog" and a live aria-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 with aria-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 from name. A picker with name="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::radio name="brand-color" value="#0f766e"> gives the whole set one tab stop and arrow-key selection, which is what a pick-one-of-N palette is; <x-wirekit::button> works where a single click is enough. A presentational component — anything whose root is a <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/wirekit.php under components.color-picker. Override them globally:

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

Further Reading

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store