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.
Preview
Primary topic
Interested topics
<div class="space-y-6 text-start">
<div class="space-y-2">
<p class="text-sm font-medium text-foreground">{{ __('Primary topic') }}</p>
<x-ui.label-selector name="preview-topic">
<x-ui.label-selector.item value="design">{{ __('Design') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="engineering" checked>{{ __('Engineering') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="product">{{ __('Product') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="marketing">{{ __('Marketing') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="sales">{{ __('Sales') }}</x-ui.label-selector.item>
</x-ui.label-selector>
</div>
<div class="space-y-2">
<p class="text-sm font-medium text-foreground">{{ __('Interested topics') }}</p>
<x-ui.label-selector name="preview-topics" multiple>
<x-ui.label-selector.item value="design" checked>{{ __('Design') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="engineering">{{ __('Engineering') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="product" checked>{{ __('Product') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="marketing">{{ __('Marketing') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="sales">{{ __('Sales') }}</x-ui.label-selector.item>
</x-ui.label-selector>
</div>
</div>
Installation
php artisan ui:add label-selector
Registry contract
php artisan ui:add label-selector
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/label-selector.blade.php -
resources/views/components/ui/label-selector/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: Label Selector (`label-selector`)
Selectable chips for single or multiple choice that submit in a normal form with no JavaScript.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add label-selector
```
## Usage
```blade
<div class="space-y-6 text-start">
<div class="space-y-2">
<p class="text-sm font-medium text-foreground">{{ __('Primary topic') }}</p>
<x-ui.label-selector name="preview-topic">
<x-ui.label-selector.item value="design">{{ __('Design') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="engineering" checked>{{ __('Engineering') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="product">{{ __('Product') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="marketing">{{ __('Marketing') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="sales">{{ __('Sales') }}</x-ui.label-selector.item>
</x-ui.label-selector>
</div>
<div class="space-y-2">
<p class="text-sm font-medium text-foreground">{{ __('Interested topics') }}</p>
<x-ui.label-selector name="preview-topics" multiple>
<x-ui.label-selector.item value="design" checked>{{ __('Design') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="engineering">{{ __('Engineering') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="product" checked>{{ __('Product') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="marketing">{{ __('Marketing') }}</x-ui.label-selector.item>
<x-ui.label-selector.item value="sales">{{ __('Sales') }}</x-ui.label-selector.item>
</x-ui.label-selector>
</div>
</div>
```
## Props
- `name` (mixed, default `required`) — Declared by @props in the shipped Blade source.
- `value` (mixed, default `null`) — Submitted value for this chip's hidden radio or checkbox input.
- `multiple` (bool, default `false`) — Declared by @props in the shipped Blade source.
- `options` (array, default `[]`) — Declared by @props in the shipped Blade source.
- `disabled` (false|true, default `false`) — Documented catalog control used by the preview workbench.
- `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.
- Offering a compact set of chip-style choices, such as tags or categories, that must submit in a plain form.
- Letting users toggle single or multiple selections without any JavaScript on the server side.
## Avoid when
- Do not hide a small option set in a dropdown when recognition and comparison matter.
- Presenting options with a title, description, or icon; use choicebox instead.
- Building a long list of options; a select or combobox scales better.
## Anti-patterns
- Hiding a small comparable set in a dropdown
## Rules
- Use the `<brok:label-selector>` tag (or `<x-ui.label-selector>`) 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/label-selector
- Registry JSON (files, props, contract): https://brokui.dev/r/open/label-selector.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 | mixed | required | Declared by @props in the shipped Blade source. |
| value | mixed | null | Submitted value for this chip's hidden radio or checkbox input. |
| multiple | bool | false | Declared by @props in the shipped Blade source. |
| options | array | [] | Declared by @props in the shipped Blade source. |
| disabled | false | true | false | Documented catalog control used by the preview workbench. |
| checked | bool | false | Declared by @props in the registry Blade source. |
Slots
default— Chip label content.x-ui.label-selector.item— Installed subcomponent from the registry item.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Each chip wraps a visually hidden native radio or checkbox; the group renders radios by default and checkboxes when multiple is set.
- Selection styling is driven purely by CSS off :has(:checked) and :has(:focus-visible) on the chip label, so the group works with JavaScript disabled.
- With multiple, selected chips submit as an array via name[]; the group's options prop derives each chip's default checked state from value.
- 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.
- Offering a compact set of chip-style choices, such as tags or categories, that must submit in a plain form.
- Letting users toggle single or multiple selections without any JavaScript on the server side.
Avoid when
- Do not hide a small option set in a dropdown when recognition and comparison matter.
- Presenting options with a title, description, or icon; use choicebox instead.
- Building a long list of options; a select or combobox scales better.
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
- The chip group renders with role="group" and each chip is a real radio or checkbox input, so assistive tech announces selection state natively.
- Focus is visible via a ring on the wrapping label so keyboard users can see which chip is focused.
- 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
Livewire can update this component through forwarded wire:* attributes.
Validation
Validation support: native. Keep the error message connected with aria-describedby.
<form wire:submit="save" class="space-y-2">
<brok:label-selector
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',
'value' => null,
'multiple' => false,
'options' => [],
])
{{--
Selectable chips/tags. Single-choice uses native <input type="radio">;
multi-choice (`:multiple`) uses native <input type="checkbox">. Each input
is visually hidden (peer) inside a styled <label> chip, so the group submits
in a normal <form> with NO JavaScript and selection is shown purely via CSS
(peer-checked). Chips inherit `name`/`multiple` via @aware; the default
selection is set from `value` on the `:options` path, or by marking composed
chips with `checked`. When `multiple`, chips submit as an array via
`name="{name}[]"`.
--}}
<div
role="group"
data-slot="label-selector"
{{ $attributes->merge(['class' => 'flex flex-wrap gap-2']) }}
>
@if (! empty($options))
@foreach ($options as $optionValue => $optionLabel)
<x-ui.label-selector.item
:value="$optionValue"
:checked="$multiple
? in_array((string) $optionValue, array_map('strval', (array) $value), true)
: ($value !== null && (string) $optionValue === (string) $value)"
>{{ __($optionLabel) }}</x-ui.label-selector.item>
@endforeach
@else
{{ $slot }}
@endif
</div>
@aware([
'name' => null,
'multiple' => false,
])
@props([
'value',
'disabled' => false,
'checked' => false,
])
@php
// The native input is the "peer": visually hidden but focusable, so we get
// form submission and :checked semantics for free. Chip styling is driven
// entirely off peer state via CSS (peer-checked / peer-focus-visible).
//
// The input type/name come from the group's `multiple` flag (inherited via
// @aware). Default selection is server-rendered via the `checked` prop: the
// <x-ui.label-selector :options> path sets it from the group `value`;
// @aware cannot carry the group value into a chip because the chip's own
// `value` attribute shadows it (documented caveat), so hand-composed chips
// mark their defaults with `checked`.
$isMultiple = (bool) $multiple;
// When multiple, submit as an array so the server receives all selections.
$inputName = $name === null ? null : ($isMultiple ? $name.'[]' : $name);
// The styled element is the <label> that WRAPS the input, so we key off
// :has(:checked) / :has(:focus-visible) on the label — peer-* only reaches
// later siblings, never the input's own parent.
$chip = 'inline-flex items-center gap-2 rounded-full border border-border bg-muted px-3 py-1 '
.'text-sm font-medium text-foreground cursor-pointer transition-colors motion-reduce:transition-none '
.'has-[:checked]:bg-primary has-[:checked]:text-primary-foreground has-[:checked]:border-primary '
.'has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 has-[:focus-visible]:ring-offset-background';
$disabledClasses = $disabled ? ' opacity-50 cursor-not-allowed' : '';
@endphp
<label
data-slot="label-selector-item"
@if ($disabled) aria-disabled="true" @endif
{{ $attributes->merge(['class' => trim($chip.$disabledClasses)]) }}
>
<input
type="{{ $isMultiple ? 'checkbox' : 'radio' }}"
class="peer sr-only"
name="{{ $inputName }}"
value="{{ $value }}"
@checked($checked)
@disabled($disabled)
/>
{{ $slot }}
</label>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.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