Check List
A bullet list whose marker is a check glyph — plan features, comparison points, what-you-get rows — from a server array or item children, with a plain or chip marker, a tone, three sizes and an excluded state.
Preview
- Unlimited projects and collaborators
- Design tokens for light and dark
- Priority support with a one-day response
- Export to Blade, Livewire or plain HTML
<x-ui.check-list class="max-w-sm" :items="[
__('Unlimited projects and collaborators'),
__('Design tokens for light and dark'),
__('Priority support with a one-day response'),
__('Export to Blade, Livewire or plain HTML'),
]" />
Tone options
Size options
Installation
php artisan ui:add check-list
Registry contract
php artisan ui:add check-list
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/check-list.blade.php -
resources/views/components/ui/check-list/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: Check List (`check-list`)
A bullet list whose marker is a check glyph — plan features, comparison points, what-you-get rows — from a server array or item children, with a plain or chip marker, a tone, three sizes and an excluded state.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add check-list
```
## Usage
```blade
<x-ui.check-list class="max-w-sm" :items="[
__('Unlimited projects and collaborators'),
__('Design tokens for light and dark'),
__('Priority support with a one-day response'),
__('Export to Blade, Livewire or plain HTML'),
]" />
```
## Props
- `items` (array, default `[]`) — Rows rendered on the server. A bare string is a row; an array row is `['label', 'included' => true, 'tone' => null, 'icon' => null, 'status' => null, 'statusLabel' => null]`. Combine with, or replace by, `<x-ui.check-list.item>` children in the default slot.
- `marker` ('plain'|'chip', default `plain`) — `plain` draws the bare glyph in the tone colour; `chip` sits it in a small tinted disc.
- `tone` ('primary'|'success'|'muted'|'destructive'|'foreground', default `primary`) — Marker colour by meaning. An item may override it with its own `tone`.
- `size` ('sm'|'md'|'lg', default `md`) — Type size and row gap: `sm` (text-sm, 8px), `md` (text-sm, 12px), `lg` (text-base, 16px).
- `icon` ('check'|'cross'|'dash'|'circle', default `check`) — The glyph every row draws unless it names its own. An excluded row swaps `check` for `dash` on its own.
- `layout` ('stack'|'row', default `stack`) — `stack` is one column; `row` lays the rows out as a wrapping line (trust points under a call to action, key results beside a case study).
- `included` (bool, default `true`) — Declared by @props in the registry Blade source.
- `status` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `statusLabel` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
## Use when
- Use to summarize, sequence, or present data so users can scan it quickly.
- A pricing tier, comparison column, hero or feature split lists what is included, one short point per row.
- A row must read as excluded (a feature the plan lacks) next to the included ones — pass `included => false`.
## Avoid when
- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- The rows are label/value pairs — use `fact-list`.
- Each row is a task the visitor completes — use `todo-item` or `checkbox`.
- The list needs numbering or step semantics — use `stepper` or `timeline-steps`.
## Anti-patterns
- Adding display ornament without informational value
## Rules
- Use the `<brok:check-list>` tag (or `<x-ui.check-list>`) 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/check-list
- Registry JSON (files, props, contract): https://brokui.dev/r/open/check-list.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
<x-ui.check-list marker="chip" class="max-w-sm">
<x-ui.check-list.item>{{ __('Own the source — no runtime lock-in') }}</x-ui.check-list.item>
<x-ui.check-list.item>{{ __('Keyboard support and focus states by default') }}</x-ui.check-list.item>
<x-ui.check-list.item>{{ __('One token model for marketing and admin') }}</x-ui.check-list.item>
</x-ui.check-list>
<x-ui.check-list class="max-w-sm" :items="[
__('Up to 5 team members'),
__('10 GB of storage'),
['label' => __('Single sign-on'), 'included' => false],
['label' => __('Audit log'), 'included' => false],
]" />
Long Content
<div class="max-w-xs">
<x-ui.check-list marker="chip" :items="[
__('A deliberately long point that verifies the text wraps under itself while the marker stays on the first line, without clipping or pushing the marker out of its column.'),
__('https://example.com/a-very-long-url-that-has-no-natural-break-points-anywhere-in-it'),
__('Short point'),
]" />
</div>
<x-ui.check-list layout="row" size="sm" class="max-w-lg font-medium text-muted-foreground" :items="[__('No credit card required'), __('Cancel anytime'), __('14-day free trial')]" />
<div class="flex flex-wrap gap-10">
@foreach (['sm', 'md', 'lg'] as $size)
<x-ui.check-list :size="$size" :items="[__('Size :size', ['size' => $size]), __('Two points per list'), __('Wraps at the column')]" class="w-48" />
@endforeach
</div>
{{-- Result rows: `status` passed, failed or pending picks the glyph and the
tone, and a screen reader hears the state word before each point, so
the result is never only an icon or a colour. `statusLabel` changes the
word, as the skipped row shows. --}}
<x-ui.check-list class="max-w-sm" marker="chip" :items="[
['label' => __('Every case has a run on this revision'), 'status' => 'passed'],
['label' => __('No must-not check failed'), 'status' => 'passed'],
['label' => __('Output stays under 400 words'), 'status' => 'failed'],
['label' => __('Second reviewer approved'), 'status' => 'pending'],
['label' => __('Load test'), 'status' => 'pending', 'statusLabel' => __('Skipped')],
]" />
<div class="grid max-w-2xl gap-6 sm:grid-cols-2">
<x-ui.check-list tone="success" marker="chip" :items="[__('Passed the accessibility audit'), __('Passed the contrast check')]" />
<x-ui.check-list tone="muted" :items="[__('Optional: connect a custom domain'), __('Optional: invite your team')]" />
<x-ui.check-list tone="destructive" icon="cross" marker="chip" :items="[__('No API access'), __('No audit log')]" />
<x-ui.check-list tone="foreground" :items="[__('Ships with the starter kit'), __('Works offline')]" />
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | array | [] | Rows rendered on the server. A bare string is a row; an array row is `['label', 'included' => true, 'tone' => null, 'icon' => null, 'status' => null, 'statusLabel' => null]`. Combine with, or replace by, `<x-ui.check-list.item>` children in the default slot. |
| marker | 'plain' | 'chip' | plain | `plain` draws the bare glyph in the tone colour; `chip` sits it in a small tinted disc. |
| tone | 'primary' | 'success' | 'muted' | 'destructive' | 'foreground' | primary | Marker colour by meaning. An item may override it with its own `tone`. |
| size | 'sm' | 'md' | 'lg' | md | Type size and row gap: `sm` (text-sm, 8px), `md` (text-sm, 12px), `lg` (text-base, 16px). |
| icon | 'check' | 'cross' | 'dash' | 'circle' | check | The glyph every row draws unless it names its own. An excluded row swaps `check` for `dash` on its own. |
| layout | 'stack' | 'row' | stack | `stack` is one column; `row` lays the rows out as a wrapping line (trust points under a call to action, key results beside a case study). |
| included | bool | true | Declared by @props in the registry Blade source. |
| status | mixed | null | null | Declared by @props in the registry Blade source. |
| statusLabel | mixed | null | null | Declared by @props in the registry Blade source. |
Slots
default— `<x-ui.check-list.item>` children; each accepts `included`, `tone`, `icon`, `status` (passed, failed or pending: glyph, tone and a screen-reader state word) and `statusLabel` (replaces that word), and inherits `marker`/`size` from the list.x-ui.check-list.item— Installed subcomponent from the registry item.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Server rows and item children resolve marker, tone and size through the same `_styles.php` recipe, so a block that mixes the two cannot draw two treatments.
- An excluded row (`included => false`) drops the text and marker to the muted tone and swaps the check for a dash; a `tone` or `icon` on that row wins over the default.
- The text column is `min-w-0 flex-1`, so a long translated point wraps under itself and never pushes the marker.
- A status row (passed, failed, pending) takes the matching glyph and tone and starts its text with a visually hidden state word (data-slot="check-list-state"); an excluded row starts with "Not included". Included rows without a status read their text only, as before.
- Declares registry capability flags: a11y, authoredStateFixtures, responsive, rtl, darkMode, localized.
Guidance
Present data for rapid scanning.
Use when
- Use to summarize, sequence, or present data so users can scan it quickly.
- A pricing tier, comparison column, hero or feature split lists what is included, one short point per row.
- A row must read as excluded (a feature the plan lacks) next to the included ones — pass `included => false`.
Avoid when
- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- The rows are label/value pairs — use `fact-list`.
- Each row is a task the visitor completes — use `todo-item` or `checkbox`.
- The list needs numbering or step semantics — use `stepper` or `timeline-steps`.
Use instead
- Table for exact comparison
- Plain text for a single value
Anti-patterns
- Adding display ornament without informational value
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- Rendered as a real `<ul role="list">` of `<li>` rows so a screen reader announces the count and each point; the glyph is decorative (`aria-hidden`).
- The excluded state is conveyed by the muted text and the dash glyph for sighted readers and by a visually hidden "Not included" before the text for screen readers, never by colour alone; `data-included="false"` is exposed for styling and tests.
- A status row names its result in text (Passed, Failed, Pending, or statusLabel) before the point, because the marker glyph is aria-hidden and colour alone carries no meaning to assistive technology.
- 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.
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
{{--
Check List — the bullet list whose marker is a check glyph: the features
a plan includes, the points a comparison column makes, the "what you get"
row under a hero. One `<ul>`, one marker treatment, one tone, instead of
the five check paths and four icon treatments the blocks used to draw.
Two sources, one markup:
- `items` — a PHP array. A bare string is a row; an array row is
`['label', 'included' => true, 'tone' => null, 'icon' => null,
'status' => null, 'statusLabel' => null]`.
- the default slot — `<x-ui.check-list.item>` children, for a row that
carries more than a string (a badge, a tooltip, a translated label
the block builds itself).
`marker` sets the treatment: `plain` is the bare glyph in the tone colour,
`chip` sits it in a small tinted disc. `tone` colours the marker by
meaning (primary, success, muted, destructive). `size` scales the type
and the row gap. `icon` picks the glyph (check, cross, dash); an excluded
row (`included => false`) turns muted and swaps the check for a dash.
A result row (`status` passed, failed or pending) takes its own glyph and
tone; excluded and status rows say their state to screen readers.
`layout="row"` lays the rows out as a wrapping line — the trust points
under a call to action, the key results beside a case study.
--}}
@props([
'items' => [],
'marker' => 'plain',
'tone' => 'primary',
'size' => 'md',
'icon' => 'check',
'layout' => 'stack',
])
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$recipe = $styles['check-list'] ?? [];
$marker = array_key_exists($marker, $recipe['markers'] ?? []) ? $marker : 'plain';
$tone = array_key_exists($tone, $recipe['tones'] ?? []) ? $tone : 'primary';
$size = array_key_exists($size, $recipe['sizes'] ?? []) ? $size : 'md';
$layout = array_key_exists($layout, $recipe['layouts'] ?? []) ? $layout : 'stack';
$listClass = trim(($recipe['layouts'][$layout] ?? '').' '.($recipe['sizes'][$size]['list'] ?? '').($layout === 'stack' ? ' '.($recipe['sizes'][$size]['stack'] ?? '') : ''));
// Normalise the array shape once so the loop stays a plain readout: a bare
// string is a row, an array row may carry its own tone/icon/included flag.
$rows = collect((array) $items)
->map(fn ($entry): array => is_array($entry)
? [
'label' => (string) ($entry['label'] ?? $entry['text'] ?? ''),
'included' => (bool) ($entry['included'] ?? true),
'tone' => $entry['tone'] ?? null,
'icon' => $entry['icon'] ?? null,
'status' => $entry['status'] ?? null,
'statusLabel' => $entry['statusLabel'] ?? null,
]
: ['label' => (string) $entry, 'included' => true, 'tone' => null, 'icon' => null, 'status' => null, 'statusLabel' => null])
->values();
@endphp
<ul
data-slot="check-list"
data-marker="{{ $marker }}"
data-tone="{{ $tone }}"
data-size="{{ $size }}"
data-layout="{{ $layout }}"
role="list"
{{ $attributes->merge(['class' => $listClass]) }}
>
@foreach ($rows as $row)
<x-ui.check-list.item
:marker="$marker"
:tone="$row['tone'] ?? ($row['status'] !== null ? null : ($row['included'] ? $tone : 'muted'))"
:size="$size"
:icon="$row['icon'] ?? $icon"
:included="$row['included']"
:status="$row['status']"
:status-label="$row['statusLabel']"
>{{ $row['label'] }}</x-ui.check-list.item>
@endforeach
{{ $slot }}
</ul>
@aware([
'marker' => 'plain',
'tone' => 'primary',
'size' => 'md',
'icon' => 'check',
])
{{-- `marker`, `size` and `icon` are inherited from the list through @aware and
may also be set per row, so they are declared as props too: a prop
declaration is what keeps an explicitly passed value from being stripped
off the aware variable. `tone` stays off the prop list on purpose — reading
it from the bag tells an explicit row tone apart from the inherited one. --}}
@props([
'included' => true,
'marker' => 'plain',
'size' => 'md',
'icon' => 'check',
// A result row: passed, failed or pending. Sets the glyph and tone
// (check/success, cross/destructive, circle/muted) and a screen-reader
// word before the text, so the state is never only an icon or a colour.
'status' => null,
// Replaces the screen-reader word of a status or excluded row.
'statusLabel' => null,
])
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$recipe = $styles['check-list'] ?? [];
$marker = array_key_exists((string) $marker, $recipe['markers'] ?? []) ? $marker : 'plain';
$size = array_key_exists((string) $size, $recipe['sizes'] ?? []) ? $size : 'md';
$icon = is_string($icon) && $icon !== '' ? $icon : 'check';
$included = filter_var($included, FILTER_VALIDATE_BOOLEAN);
// An excluded row reads as "not in this plan": muted marker, dash glyph,
// muted text — unless the caller names a tone or glyph for it explicitly.
$rowTone = $attributes->get('tone');
$explicitTone = $rowTone !== null;
$tone = $rowTone ?? ($tone ?? 'primary');
$tone = array_key_exists((string) $tone, $recipe['tones'] ?? []) ? $tone : 'primary';
$resolvedTone = $included || $explicitTone ? $tone : 'muted';
$resolvedIcon = $included ? $icon : ($icon === 'check' ? 'dash' : $icon);
// A status row picks its own glyph and tone unless the row names them.
$statuses = [
'passed' => ['icon' => 'check', 'tone' => 'success', 'label' => __('Passed')],
'failed' => ['icon' => 'cross', 'tone' => 'destructive', 'label' => __('Failed')],
'pending' => ['icon' => 'circle', 'tone' => 'muted', 'label' => __('Pending')],
];
$status = is_string($status) && array_key_exists($status, $statuses) ? $status : null;
if ($status !== null) {
$resolvedTone = $explicitTone ? $tone : $statuses[$status]['tone'];
$resolvedIcon = $icon !== 'check' ? $icon : $statuses[$status]['icon'];
}
// The words a screen reader hears before the text: the status, or "Not
// included" on an excluded row, whose dash and muted colour are not read.
$stateText = match (true) {
filled($statusLabel) => (string) $statusLabel,
$status !== null => $statuses[$status]['label'],
! $included => __('Not included'),
default => null,
};
$glyphs = [
'check' => '<path d="M20 6 9 17l-5-5" />',
'cross' => '<path d="M18 6 6 18M6 6l12 12" />',
'dash' => '<path d="M5 12h14" />',
'circle' => '<circle cx="12" cy="12" r="8" />',
];
$glyph = $glyphs[$resolvedIcon] ?? $glyphs['check'];
$sizes = $recipe['sizes'][$size] ?? [];
$toneClass = $recipe['tones'][$resolvedTone][$marker] ?? '';
$markerClass = trim(($recipe['markers'][$marker] ?? '').' '.$toneClass.' '.($sizes[$marker === 'chip' ? 'chip-box' : 'glyph'] ?? ''));
$svgClass = $marker === 'chip' ? ($sizes['chip-glyph'] ?? 'size-3') : 'size-full';
@endphp
<li
data-slot="check-list-item"
@if (! $included) data-included="false" @endif
@if ($status !== null) data-status="{{ $status }}" @endif
{{ $attributes->except('tone')->merge(['class' => trim(($recipe['item'] ?? '').' '.($sizes[$marker] ?? '').($included ? '' : ' '.($recipe['excluded'] ?? '')))]) }}
>
<span data-slot="check-list-marker" class="{{ $markerClass }}" aria-hidden="true">
<svg class="{{ $svgClass }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{!! $glyph !!}</svg>
</span>
<span data-slot="check-list-text" class="{{ $recipe['text'] ?? 'min-w-0 flex-1' }}">@if ($stateText !== null)<span data-slot="check-list-state" class="sr-only">{{ __(':state:', ['state' => $stateText]) }} </span>@endif{{ $slot }}</span>
</li>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.1.0 - 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