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.0.3
Stability
stable
License
MIT
Related
Choicebox
Radio Rows
Radio Tabs
Color Swatch Selector
4 of 5

Label Selector — Selectable chips for single or multiple choice that submit in a normal form with no JavaScript.

Preview

Primary topic

Interested topics

Disabled
previews.components.label-selector.default.blade.php 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>

Installation

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

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

label-selector.md
# 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

manifest knowledge + registry-derived coverage

Props

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

label-selector label-selector-item

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

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.
  • 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
root label-selector-item
Theming hooks
label-selector label-selector-item

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
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-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

Safe

Livewire can update this component through forwarded wire:* attributes.

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

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