Choice
Pick-one and pick-many controls built on native radios and checkboxes — a card, a full-width row, a segmented tab, a chip or a colour swatch.
Color Swatch Selector — Single-choice colour picker built from visually-hidden native radios; the selected chip shows a ring, no JavaScript. Chips take a theme token class or an arbitrary CSS colour, in three sizes and two shapes.
Preview
<div class="flex w-full max-w-sm flex-col gap-6">
<fieldset class="flex flex-col gap-2">
<legend class="text-sm font-medium text-foreground">{{ __('Accent') }}</legend>
<x-ui.color-swatch
name="accent"
selected="orange"
:colors="[
['value' => 'red', 'label' => __('Red'), 'color' => '#ef4444'],
['value' => 'orange', 'label' => __('Orange'), 'color' => '#f97316'],
['value' => 'green', 'label' => __('Green'), 'color' => '#22c55e'],
['value' => 'blue', 'label' => __('Blue'), 'color' => '#3b82f6'],
['value' => 'violet', 'label' => __('Violet'), 'color' => '#8b5cf6'],
['value' => 'slate', 'label' => __('Slate'), 'color' => '#64748b', 'disabled' => true],
]"
/>
</fieldset>
<fieldset class="flex flex-col gap-2">
<legend class="text-sm font-medium text-foreground">{{ __('Themed tokens, square, small') }}</legend>
<x-ui.color-swatch
name="surface"
selected="primary"
size="sm"
shape="square"
:colors="[
['value' => 'primary', 'label' => __('Primary'), 'token' => 'bg-primary'],
['value' => 'secondary', 'label' => __('Secondary'), 'token' => 'bg-secondary'],
['value' => 'muted', 'label' => __('Muted'), 'token' => 'bg-muted'],
['value' => 'accent', 'label' => __('Accent'), 'token' => 'bg-accent'],
]"
/>
</fieldset>
</div>
Size options
Shape options
Installation
php artisan ui:add color-swatch
Registry contract
php artisan ui:add color-swatch
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/color-swatch.blade.php -
resources/views/components/ui/color-swatch/item.blade.php
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Color Swatch Selector (`color-swatch`)
Single-choice colour picker built from visually-hidden native radios; the selected chip shows a ring, no JavaScript. Chips take a theme token class or an arbitrary CSS colour, in three sizes and two shapes.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add color-swatch
```
## Usage
```blade
<div class="flex w-full max-w-sm flex-col gap-6">
<fieldset class="flex flex-col gap-2">
<legend class="text-sm font-medium text-foreground">{{ __('Accent') }}</legend>
<x-ui.color-swatch
name="accent"
selected="orange"
:colors="[
['value' => 'red', 'label' => __('Red'), 'color' => '#ef4444'],
['value' => 'orange', 'label' => __('Orange'), 'color' => '#f97316'],
['value' => 'green', 'label' => __('Green'), 'color' => '#22c55e'],
['value' => 'blue', 'label' => __('Blue'), 'color' => '#3b82f6'],
['value' => 'violet', 'label' => __('Violet'), 'color' => '#8b5cf6'],
['value' => 'slate', 'label' => __('Slate'), 'color' => '#64748b', 'disabled' => true],
]"
/>
</fieldset>
<fieldset class="flex flex-col gap-2">
<legend class="text-sm font-medium text-foreground">{{ __('Themed tokens, square, small') }}</legend>
<x-ui.color-swatch
name="surface"
selected="primary"
size="sm"
shape="square"
:colors="[
['value' => 'primary', 'label' => __('Primary'), 'token' => 'bg-primary'],
['value' => 'secondary', 'label' => __('Secondary'), 'token' => 'bg-secondary'],
['value' => 'muted', 'label' => __('Muted'), 'token' => 'bg-muted'],
['value' => 'accent', 'label' => __('Accent'), 'token' => 'bg-accent'],
]"
/>
</fieldset>
</div>
```
## Props
- `name` (string, default `color`) — Form name shared by every radio in the group. Inherited by composed children via @aware, so give two groups on one page distinct names.
- `value` (mixed|null, default `null`) — Declared by @props in the shipped Blade source.
- `selected` (string|null, default `null`) — Value of the initially selected swatch. `value` is the pre-1.2 alias and still works.
- `colors` (array, default `[]`) — Swatch entries. Each is ['value','label','token'|'color','disabled']: `value` is submitted (falls back to `label`), `token` is a Tailwind background class such as `bg-primary` (preferred — the theme owns it), `color` is an arbitrary CSS colour carried as an inline style. Omit to compose <x-ui.color-swatch.item> children by hand.
- `size` (sm|md|lg, default `md`) — Chip diameter — 24 / 32 / 40px. Set it on the group; children inherit it.
- `shape` (circle|square, default `circle`) — Chip silhouette. Set it on the group; children inherit it.
- `model` (string|null, default `null`) — Alpine/Livewire property bound to the group with x-model, for client state instead of a form post.
- `label` (string|null, default `null`) — Accessible name for the radiogroup. Give it one whenever no visible <legend> names the group.
- `disabled` (bool, default `false`) — Disables every swatch in the group. A single entry can opt out on its own with 'disabled' => true.
- `color` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `token` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `checked` (bool, default `false`) — Declared by @props in the registry Blade source.
## Use when
- Use for a small set of mutually exclusive options that users should compare visibly before choosing.
- Picking one colour from a small, visible set — a product colourway, a theme accent, a label colour.
- The choice must post in a plain form or bind to Alpine/Livewire without a custom control.
## Avoid when
- Do not hide a small option set in a dropdown when recognition and comparison matter.
- The palette is open-ended — use a native <input type="color"> or a full picker.
- The options are words rather than colours (sizes, finishes) — use <x-ui.option-group>.
- More than one option can be picked at a time; these are radios by design.
## Anti-patterns
- Hiding a small comparable set in a dropdown
## Rules
- Use the `<brok:color-swatch>` tag (or `<x-ui.color-swatch>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/color-swatch
- Registry JSON (files, props, contract): https://brokui.dev/r/open/color-swatch.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | color | Form name shared by every radio in the group. Inherited by composed children via @aware, so give two groups on one page distinct names. |
| value | mixed | null | null | Declared by @props in the shipped Blade source. |
| selected | string | null | null | Value of the initially selected swatch. `value` is the pre-1.2 alias and still works. |
| colors | array | [] | Swatch entries. Each is ['value','label','token'|'color','disabled']: `value` is submitted (falls back to `label`), `token` is a Tailwind background class such as `bg-primary` (preferred — the theme owns it), `color` is an arbitrary CSS colour carried as an inline style. Omit to compose <x-ui.color-swatch.item> children by hand. |
| size | sm | md | lg | md | Chip diameter — 24 / 32 / 40px. Set it on the group; children inherit it. |
| shape | circle | square | circle | Chip silhouette. Set it on the group; children inherit it. |
| model | string | null | null | Alpine/Livewire property bound to the group with x-model, for client state instead of a form post. |
| label | string | null | null | Accessible name for the radiogroup. Give it one whenever no visible <legend> names the group. |
| disabled | bool | false | Disables every swatch in the group. A single entry can opt out on its own with 'disabled' => true. |
| color | mixed | null | null | Declared by @props in the registry Blade source. |
| token | mixed | null | null | Declared by @props in the registry Blade source. |
| checked | bool | false | Declared by @props in the registry Blade source. |
Slots
default— Primary Blade slot rendered by the component.x-ui.color-swatch.item— Installed subcomponent from the registry item.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Uses server-rendered Blade and semantic HTML.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Choose one option from a small visible set.
Use when
- Use for a small set of mutually exclusive options that users should compare visibly before choosing.
- Picking one colour from a small, visible set — a product colourway, a theme accent, a label colour.
- The choice must post in a plain form or bind to Alpine/Livewire without a custom control.
Avoid when
- Do not hide a small option set in a dropdown when recognition and comparison matter.
- The palette is open-ended — use a native <input type="color"> or a full picker.
- The options are words rather than colours (sizes, finishes) — use <x-ui.option-group>.
- More than one option can be picked at a time; these are radios by design.
Use instead
- Select or combobox for long option sets
Anti-patterns
- Hiding a small comparable set in a dropdown
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- native
- Focus
native
- A role="radiogroup" of visually hidden native radios inside labels: arrow keys move the choice and the value submits with the form without JavaScript.
- Each chip has an accessible name (its label, or the value); give the group a label that names what the colour is for.
- The selected chip shows a ring, not colour alone; a disabled group sets aria-disabled.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="color-swatch-{{ $record->id }}">
<div class="flex w-full max-w-sm flex-col gap-6">
<fieldset class="flex flex-col gap-2">
<legend class="text-sm font-medium text-foreground">{{ __('Accent') }}</legend>
<x-ui.color-swatch
name="accent"
selected="orange"
:colors="[
['value' => 'red', 'label' => __('Red'), 'color' => '#ef4444'],
['value' => 'orange', 'label' => __('Orange'), 'color' => '#f97316'],
['value' => 'green', 'label' => __('Green'), 'color' => '#22c55e'],
['value' => 'blue', 'label' => __('Blue'), 'color' => '#3b82f6'],
['value' => 'violet', 'label' => __('Violet'), 'color' => '#8b5cf6'],
['value' => 'slate', 'label' => __('Slate'), 'color' => '#64748b', 'disabled' => true],
]"
/>
</fieldset>
<fieldset class="flex flex-col gap-2">
<legend class="text-sm font-medium text-foreground">{{ __('Themed tokens, square, small') }}</legend>
<x-ui.color-swatch
name="surface"
selected="primary"
size="sm"
shape="square"
:colors="[
['value' => 'primary', 'label' => __('Primary'), 'token' => 'bg-primary'],
['value' => 'secondary', 'label' => __('Secondary'), 'token' => 'bg-secondary'],
['value' => 'muted', 'label' => __('Muted'), 'token' => 'bg-muted'],
['value' => 'accent', 'label' => __('Accent'), 'token' => 'bg-accent'],
]"
/>
</fieldset>
</div>
</div>
Validation
Validation support: native. Keep the error message connected with aria-describedby.
<form wire:submit="save" class="space-y-2">
<brok:color-swatch
wire:model="value"
:aria-invalid="$errors->has('value') ? 'true' : 'false'"
aria-describedby="value-error"
/>
@error('value')
<p id="value-error" role="alert">{{ $message }}</p>
@enderror
<brok:button type="submit" wire:loading.attr="disabled">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving…</span>
</brok:button>
</form>
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'name' => 'color',
'value' => null,
'selected' => null,
'colors' => [],
'size' => 'md',
'shape' => 'circle',
'model' => null,
'label' => null,
'disabled' => false,
])
{{--
Single-choice color picker. Each swatch is a visually-hidden native
<input type="radio"> inside a styled <label>, so it submits in a normal
<form> with NO JavaScript and the selected swatch is shown purely via CSS
(peer-checked → ring). Swatches inherit `name`, `size`, `shape` and `model`
via @aware; the default selection is set from `selected` (or its older alias
`value`) on the `:colors` path, or by marking a composed swatch `checked`.
A `:colors` entry is an array: `value` (submitted; falls back to `label`),
`label` (the accessible name), and the fill — either `token`, a Tailwind
background class such as `bg-primary`, or `color`, an arbitrary CSS colour
string carried as an inline style. Prefer `token` inside a block: a literal
hex in the markup is raw colour and the theme cannot reach it.
Pass `model` to bind the group to an Alpine/Livewire property
(`<x-ui.color-swatch model="colour" …>`) instead of posting it in a form.
--}}
@php
$size = in_array($size, ['sm', 'md', 'lg'], true) ? $size : 'md';
$shape = in_array($shape, ['circle', 'square'], true) ? $shape : 'circle';
// `selected` is the current API; `value` stays as its pre-1.2 alias.
$current = $selected ?? $value;
@endphp
<div
role="radiogroup"
data-slot="color-swatch"
data-size="{{ $size }}"
data-shape="{{ $shape }}"
@if ($label !== null) aria-label="{{ $label }}" @endif
{{ $attributes->merge(['class' => 'flex flex-wrap items-center gap-2 focus-visible:ring-2 focus-visible:ring-ring']) }}
>
@if (! empty($colors))
@foreach ($colors as $key => $swatch)
@php
$swatchValue = $swatch['value'] ?? $swatch['label'] ?? $key;
$swatchLabel = $swatch['label'] ?? $swatch['name'] ?? $swatchValue;
@endphp
<x-ui.color-swatch.item
:value="$swatchValue"
:color="$swatch['color'] ?? null"
:token="$swatch['token'] ?? $swatch['class'] ?? null"
:label="__($swatchLabel)"
:size="$size"
:shape="$shape"
:model="$model"
:disabled="$disabled || ! empty($swatch['disabled'])"
:checked="$current !== null && (string) $swatchValue === (string) $current"
/>
@endforeach
@else
{{ $slot }}
@endif
</div>
@aware([
'name' => 'color',
'size' => 'md',
'shape' => 'circle',
'model' => null,
])
@props([
'value',
'color' => null,
'token' => null,
'label' => null,
'size' => 'md',
'shape' => 'circle',
'model' => null,
'disabled' => false,
'checked' => false,
])
@php
// The native radio is the "peer": visually hidden but focusable, so we get
// arrow-key navigation, form submission and :checked semantics for free.
// Selection is the ring alone: a mark inside the chip would cover the colour
// the user is choosing.
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$swatch = $styles['swatch'];
$size = in_array($size, ['sm', 'md', 'lg'], true) ? $size : 'md';
$shape = in_array($shape, ['circle', 'square'], true) ? $shape : 'circle';
// The fill is either a token class (preferred — the theme owns it) or an
// arbitrary CSS colour string handed in as data. `color` is the ONE allowed
// inline colour: it is user data, not a hardcoded palette value.
$chip = $swatch['chip'].' '.$swatch['sizes'][$size].' '.$swatch['shapes'][$shape]
.($token ? ' '.$token : '');
$labelClasses = $disabled ? $swatch['labelDisabled'] : $swatch['label'];
@endphp
<label
data-slot="color-swatch-item"
@if ($disabled) aria-disabled="true" @endif
{{ $attributes->merge(['class' => $labelClasses]) }}
>
<input
type="radio"
class="peer sr-only"
name="{{ $name }}"
value="{{ $value }}"
aria-label="{{ $label ?? $value }}"
@if (filled($model)) x-model="{{ $model }}" @endif
@checked($checked)
@disabled($disabled)
/>
<span class="{{ $chip }}" @if ($color) style="background-color: {{ $color }}" @endif></span>
</label>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.2.3 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0