Skip to content
Brok UI

Loading…

No results

Choice

Open source

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.

Version
v1.2.3
Stability
stable
License
MIT
Related
Color Token
Color Dropdown
Color Palette
5 of 5

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

Accent
Themed tokens, square, small
Size
Shape
Disabled
previews.components.color-swatch.default.blade.php 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>

Size options

Sm Current
Md Current
Lg Current

Shape options

Circle Current
Square Current

Installation

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

  • blade resources/views/components/ui/color-swatch.blade.php
  • blade 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.

color-swatch.md
# 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

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
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.

color-swatch color-swatch-item

Behavior

  • Uses server-rendered Blade and semantic HTML.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Visible exclusive selection

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
color-swatch color-swatch-item
Theming hooks
color-swatch color-swatch-item

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
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-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, 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 under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<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.

livewire-form.blade.php Blade
<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.

resources/views/components/ui/color-swatch.blade.php Blade
@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>
resources/views/components/ui/color-swatch/item.blade.php Blade
@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