List Panel
A card-wrapped panel with an optional header and a divided list of rows, each with a leading visual, primary and secondary text, and trailing meta.
Preview
Recent activity
- Acme Inc. Enterprise plan
- M Mara Sundström Mara Sundström Product designer Owner
- Q3 roadmap.pdf Updated 2 days ago 2.1 MB
-
Security review Quarterly audit
Transactions
-
N Northwind Northwind Subscription -$220.36 2 min ago
-
A Acme Acme Subscription -$110.00 10 min ago
-
L Lumen Studio Lumen Studio Subscription -$220.36 2h ago
-
A Alex D. Alex D. Payment -$220.36 Yesterday
-
H Harbor Payroll Harbor Payroll Payment +$3,500.00 12 Jul 2026
{{-- Rows with `href` are links (hover fill, chevron); `tone` renders the meta as a badge. --}}
<div class="mx-auto flex w-full max-w-md flex-col gap-6">
<x-ui.list-panel
:title="__('Recent activity')"
:rows="[
['title' => __('Acme Inc.'), 'subtitle' => __('Enterprise plan'), 'meta' => __('Active'), 'tone' => 'positive', 'icon' => 'folder', 'href' => '#'],
['title' => __('Mara Sundström'), 'subtitle' => __('Product designer'), 'meta' => __('Owner'), 'href' => '#'],
['title' => __('Q3 roadmap.pdf'), 'subtitle' => __('Updated 2 days ago'), 'meta' => __('2.1 MB'), 'icon' => 'file', 'href' => '#'],
['title' => __('Security review'),'subtitle' => __('Quarterly audit'), 'meta' => __('Pending'), 'tone' => 'attention', 'icon' => 'bell'],
]"
>
<x-slot:action><a href="#">{{ __('View all') }}</a></x-slot:action>
</x-ui.list-panel>
{{-- `detail` stacks a second, muted line under `meta`; `metaTone` tints a signed
amount with the same success/destructive tokens `stat`/`money-value` use.
The sign always stays in the text, never colour alone. --}}
<x-ui.list-panel
:title="__('Transactions')"
:rows="[
['title' => __('Northwind'), 'subtitle' => __('Subscription'), 'meta' => __('-$220.36'), 'metaTone' => 'negative', 'detail' => __('2 min ago')],
['title' => __('Acme'), 'subtitle' => __('Subscription'), 'meta' => __('-$110.00'), 'metaTone' => 'negative', 'detail' => __('10 min ago')],
['title' => __('Lumen Studio'), 'subtitle' => __('Subscription'), 'meta' => __('-$220.36'), 'metaTone' => 'negative', 'detail' => __('2h ago')],
['title' => __('Alex D.'), 'subtitle' => __('Payment'), 'meta' => __('-$220.36'), 'metaTone' => 'negative', 'detail' => __('Yesterday')],
['title' => __('Harbor Payroll'), 'subtitle' => __('Payment'), 'meta' => __('+$3,500.00'), 'metaTone' => 'positive', 'detail' => __('12 Jul 2026')],
]"
>
<x-slot:action><a href="#">{{ __('View all') }}</a></x-slot:action>
</x-ui.list-panel>
</div>
Installation
php artisan ui:add list-panel
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: List Panel (`list-panel`)
A card-wrapped panel with an optional header and a divided list of rows, each with a leading visual, primary and secondary text, and trailing meta.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add list-panel
```
## Usage
```blade
{{-- Rows with `href` are links (hover fill, chevron); `tone` renders the meta as a badge. --}}
<div class="mx-auto flex w-full max-w-md flex-col gap-6">
<x-ui.list-panel
:title="__('Recent activity')"
:rows="[
['title' => __('Acme Inc.'), 'subtitle' => __('Enterprise plan'), 'meta' => __('Active'), 'tone' => 'positive', 'icon' => 'folder', 'href' => '#'],
['title' => __('Mara Sundström'), 'subtitle' => __('Product designer'), 'meta' => __('Owner'), 'href' => '#'],
['title' => __('Q3 roadmap.pdf'), 'subtitle' => __('Updated 2 days ago'), 'meta' => __('2.1 MB'), 'icon' => 'file', 'href' => '#'],
['title' => __('Security review'),'subtitle' => __('Quarterly audit'), 'meta' => __('Pending'), 'tone' => 'attention', 'icon' => 'bell'],
]"
>
<x-slot:action><a href="#">{{ __('View all') }}</a></x-slot:action>
</x-ui.list-panel>
{{-- `detail` stacks a second, muted line under `meta`; `metaTone` tints a signed
amount with the same success/destructive tokens `stat`/`money-value` use.
The sign always stays in the text, never colour alone. --}}
<x-ui.list-panel
:title="__('Transactions')"
:rows="[
['title' => __('Northwind'), 'subtitle' => __('Subscription'), 'meta' => __('-$220.36'), 'metaTone' => 'negative', 'detail' => __('2 min ago')],
['title' => __('Acme'), 'subtitle' => __('Subscription'), 'meta' => __('-$110.00'), 'metaTone' => 'negative', 'detail' => __('10 min ago')],
['title' => __('Lumen Studio'), 'subtitle' => __('Subscription'), 'meta' => __('-$220.36'), 'metaTone' => 'negative', 'detail' => __('2h ago')],
['title' => __('Alex D.'), 'subtitle' => __('Payment'), 'meta' => __('-$220.36'), 'metaTone' => 'negative', 'detail' => __('Yesterday')],
['title' => __('Harbor Payroll'), 'subtitle' => __('Payment'), 'meta' => __('+$3,500.00'), 'metaTone' => 'positive', 'detail' => __('12 Jul 2026')],
]"
>
<x-slot:action><a href="#">{{ __('View all') }}</a></x-slot:action>
</x-ui.list-panel>
</div>
```
## Props
- `title` (mixed|null, default `null`) — Optional header title shown above the list.
- `action` (mixed|null, default `null`) — Content at the end of the header row, typically a "View all" link or a small button. Pass it as a prop or as the `action` slot; it shows only with a title.
- `rows` (mixed|null, default `null`) — Array of row tuples (title, subtitle, meta, detail, metaTone, avatar, icon, href, tone, actions) to render; defaults to a sample list. `detail` adds a second, muted trailing line under `meta` (e.g. a relative time under a signed amount); `metaTone` (positive|negative|neutral) tints `meta` with the success/destructive text tokens, never as the only signal. `actions` takes row actions in the row-actions format (key, label, kind link|submit|dialog, url, quick, icon); they render after the row content, outside any row link, with the row title in their accessible names.
- `divided` (bool, default `true`) — Draws a divider between rows.
## Use when
- Use to summarize, sequence, or present data so users can scan it quickly.
- Showing a short, card-wrapped list of records, such as recent activity, files, or team members, each with a leading visual and optional trailing meta.
- Linking each row to a detail page while keeping the whole list inside one card.
## Avoid when
- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- The list needs sorting, filtering, or pagination; use table or livewire-data-table instead.
## Anti-patterns
- Adding display ornament without informational value
## Rules
- Use the `<brok:list-panel>` tag (or `<x-ui.list-panel>`) 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/list-panel
- Registry JSON (files, props, contract): https://brokui.dev/r/open/list-panel.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
Row Actions
{{-- `actions` per row uses the row-actions format: a `quick` action shows inline,
the rest sit behind the More menu. They render beside the row link, never
inside it, and their accessible names repeat the row title. --}}
<div class="mx-auto flex w-full max-w-md flex-col gap-6">
<x-ui.list-panel
:title="__('Connected clients')"
:rows="[
['title' => __('Claude Desktop'), 'subtitle' => __('Last used 2 minutes ago'), 'icon' => 'message', 'href' => '#', 'actions' => [
['key' => 'revoke', 'label' => __('Revoke'), 'kind' => 'submit', 'url' => '#', 'quick' => true],
['key' => 'details', 'label' => __('View details'), 'url' => '#'],
]],
['title' => __('Research agent'), 'subtitle' => __('Last used yesterday'), 'icon' => 'user', 'actions' => [
['key' => 'revoke', 'label' => __('Revoke'), 'kind' => 'submit', 'url' => '#', 'quick' => true],
]],
['title' => __('Browser extension'), 'subtitle' => __('Never used'), 'meta' => __('New'), 'tone' => 'info', 'icon' => 'star'],
]"
>
<x-slot:action><a href="#">{{ __('Manage') }}</a></x-slot:action>
</x-ui.list-panel>
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | mixed | null | null | Optional header title shown above the list. |
| action | mixed | null | null | Content at the end of the header row, typically a "View all" link or a small button. Pass it as a prop or as the `action` slot; it shows only with a title. |
| rows | mixed | null | null | Array of row tuples (title, subtitle, meta, detail, metaTone, avatar, icon, href, tone, actions) to render; defaults to a sample list. `detail` adds a second, muted trailing line under `meta` (e.g. a relative time under a signed amount); `metaTone` (positive|negative|neutral) tints `meta` with the success/destructive text tokens, never as the only signal. `actions` takes row actions in the row-actions format (key, label, kind link|submit|dialog, url, quick, icon); they render after the row content, outside any row link, with the row title in their accessible names. |
| divided | bool | true | Draws a divider between rows. |
Slots
default—action— End of the header row (with a title): a "View all" link or a small button. Same as the action prop, for markup.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Renders each row as an avatar image, a fixed icon, or an initials fallback, followed by title, optional subtitle, and optional trailing meta or tone badge.
- A row becomes a link, with hover fill, a focus ring, and a trailing chevron, whenever it carries an href.
- A row with `detail` stacks it under `meta` as a second, smaller, muted, end-aligned line (e.g. a signed amount over a relative time); `meta` reads as the prominent value in that stack. `meta` always keeps tabular numerals and never wraps; the title/subtitle truncate first so the trailing stack is never the one that breaks.
- A row with `actions` keeps its link or text area and puts the actions beside it: quick actions inline, the rest behind a More menu (row-actions). Rows without actions render exactly as before.
- Declares registry capability flags: a11y, 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.
- Showing a short, card-wrapped list of records, such as recent activity, files, or team members, each with a leading visual and optional trailing meta.
- Linking each row to a detail page while keeping the whole list inside one card.
Avoid when
- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- The list needs sorting, filtering, or pagination; use table or livewire-data-table instead.
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
- The list renders as a semantic ul/li with role="list", so it is announced as a list even where a browser would otherwise strip list semantics from a styled ul.
- metaTone only tints text colour; the sign or word stays in `meta`'s text so the row never relies on colour alone.
- 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 file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'title' => null,
'action' => null,
'rows' => null,
'divided' => true,
])
{{--
List Panel — a card-wrapped vertical list (shadcnblocks "List Panel" pattern).
A panel with an optional header (title + optional action) and a divided list
of rows. Each row composes a leading visual (avatar image, fallback initials,
or a fixed inline icon), primary text, optional secondary subtext, and optional
trailing meta. Rows are rendered as a semantic <ul>/<li> and long text truncates.
Each row tuple may carry:
'title' — primary text (required)
'subtitle' — secondary subtext (optional)
'meta' — trailing meta string (optional)
'detail' — a second trailing line under `meta`, muted and smaller, for a
relative time or a secondary amount (optional)
'metaTone' — positive|negative|neutral: tints `meta` with the same
success/destructive text tokens `stat` and `money-value` use
for signed amounts. Never the only signal — the sign stays in
the text. Ignored when `tone` is also set (optional)
'avatar' — image url for the leading visual, shown on a neutral circle
(optional)
'icon' — fixed icon key from the map below (optional)
'href' — the row becomes a link (hover fill, focus ring, a trailing chevron) (optional)
'tone' — positive|attention|negative|info|neutral: renders `meta` as a tone badge (optional)
'actions' — row actions in the row-actions format ([['key', 'label', 'kind',
'url', 'quick', …]]). They render after the row content, never
inside a row link, and their accessible names repeat the row
title. A row without actions renders exactly as before (optional)
The `action` slot (or prop) fills the end of the header row, typically a
"View all" link or a small button.
No raw colour, no spliced arbitrary classes. RTL-safe via logical properties
(the trailing chevron flips under dir="rtl" via rtl:rotate-180).
--}}
@php
// Fixed inline icon set. stroke="currentColor" so the chip's text token drives
// the colour. Keyed by the row 'icon' value (JIT-irrelevant — pure markup).
$icons = [
'user' => '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
'file' => '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v5h5"/>',
'folder' => '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
'message' => '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
'bell' => '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',
'star' => '<path d="M11.5 2.6a.5.5 0 0 1 .9 0l2.5 5 5.6.8a.5.5 0 0 1 .3.9l-4 4 1 5.6a.5.5 0 0 1-.8.5L12 17l-5 2.7a.5.5 0 0 1-.8-.5l1-5.6-4-4a.5.5 0 0 1 .3-.9l5.6-.8z"/>',
];
$rows ??= [
['title' => __('Acme Inc.'), 'subtitle' => __('Enterprise plan'), 'meta' => __('Active'), 'icon' => 'folder'],
['title' => __('Mara Sundström'), 'subtitle' => __('Product designer'), 'meta' => __('Owner'), 'icon' => 'user'],
['title' => __('Q3 roadmap.pdf'), 'subtitle' => __('Updated 2 days ago'), 'meta' => __('2.1 MB'), 'icon' => 'file'],
['title' => __('Security review'), 'subtitle' => __('Quarterly audit'), 'meta' => __('Pending'), 'icon' => 'bell'],
];
$rowList = array_values((array) $rows);
@endphp
<x-ui.card {{ $attributes->merge(['class' => 'overflow-hidden p-0']) }} data-slot="list-panel">
@if ($title)
<div class="flex items-center justify-between gap-3 border-b border-border px-4 py-3">
<h3 class="truncate text-sm font-semibold text-foreground">{{ $title }}</h3>
@isset($action)
<div class="shrink-0 text-sm text-muted-foreground [&_a]:underline-offset-4 [&_a:hover]:text-foreground [&_a:hover]:underline">{{ $action }}</div>
@endisset
</div>
@endif
<ul role="list" data-slot="list-panel-list">
@foreach ($rowList as $row)
@php
$rowTitle = (string) ($row['title'] ?? '');
$rowSubtitle = $row['subtitle'] ?? null;
$rowMeta = $row['meta'] ?? null;
$rowAvatar = $row['avatar'] ?? null;
$rowIconKey = $row['icon'] ?? null;
$rowIconSvg = $rowIconKey ? ($icons[$rowIconKey] ?? null) : null;
$rowActions = array_values(array_filter((array) ($row['actions'] ?? []), 'is_array'));
@endphp
@php
$rowHref = $row['href'] ?? null;
$rowTone = $row['tone'] ?? null;
$rowTag = $rowHref ? 'a' : 'div';
$rowDetail = $row['detail'] ?? null;
$rowHasDetail = $rowDetail !== null && $rowDetail !== '';
$rowMetaTone = $row['metaTone'] ?? null;
$rowMetaTone = in_array($rowMetaTone, ['positive', 'negative', 'neutral'], true) ? $rowMetaTone : null;
// The two-line stack (a signed amount over a relative time) reads
// the value line as prominent; a bare `meta` keeps its v1.1 look.
$metaValueClass = match (true) {
$rowMetaTone === 'positive' => 'text-success-text',
$rowMetaTone === 'negative' => 'text-destructive-text',
$rowHasDetail => 'text-foreground',
default => 'text-muted-foreground',
};
$metaSizeClass = $rowHasDetail ? 'text-sm font-medium' : 'text-xs';
@endphp
<li data-slot="list-panel-item" @class(['border-b border-border last:border-b-0' => $divided, 'flex min-w-0 items-center' => $rowActions !== []])>
<{{ $rowTag }}
@if ($rowHref) href="{{ $rowHref }}" @endif
@class([
'flex items-center gap-3 px-4 py-3' => $rowActions === [],
'flex min-w-0 flex-1 items-center gap-3 py-3 ps-4 pe-2' => $rowActions !== [],
'transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-inset focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none' => $rowHref,
])
>
{{-- Leading visual: avatar image, then icon chip, then initials fallback. --}}
@if ($rowAvatar)
<x-ui.avatar :src="$rowAvatar" :alt="$rowTitle" size="sm" class="shrink-0" />
@elseif ($rowIconSvg)
<span class="flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4">{!! $rowIconSvg !!}</svg>
</span>
@else
<x-ui.avatar :alt="$rowTitle" size="sm" class="shrink-0" :fallback="mb_strtoupper(mb_substr($rowTitle, 0, 1))" />
@endif
{{-- Primary + secondary text. min-w-0 lets both lines truncate. --}}
<span class="min-w-0 flex-1">
<span class="block truncate text-sm font-medium text-foreground">{{ $rowTitle }}</span>
@if ($rowSubtitle)
<span class="block truncate text-sm text-muted-foreground">{{ $rowSubtitle }}</span>
@endif
</span>
{{-- Trailing meta (a tone badge when the row says so, otherwise a value
optionally paired with a muted `detail` line underneath), then a
chevron on links. --}}
@if ($rowMeta !== null && $rowMeta !== '')
@if ($rowTone)
<x-ui.badge :tone="$rowTone" size="sm" class="ms-auto shrink-0">{{ $rowMeta }}</x-ui.badge>
@else
<span class="ms-auto flex shrink-0 flex-col items-end {{ $rowHasDetail ? 'gap-0.5' : '' }}">
<span class="whitespace-nowrap tabular-nums {{ $metaSizeClass }} {{ $metaValueClass }}">{{ $rowMeta }}</span>
@if ($rowHasDetail)
<span class="whitespace-nowrap text-2xs text-muted-foreground">{{ $rowDetail }}</span>
@endif
</span>
@endif
@endif
@if ($rowHref)
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0 text-muted-foreground rtl:rotate-180 {{ ($rowMeta !== null && $rowMeta !== '') ? '' : 'ms-auto' }}" aria-hidden="true">
<path d="m9 18 6-6-6-6" />
</svg>
@endif
</{{ $rowTag }}>
@if ($rowActions !== [])
<div data-slot="list-panel-actions" class="flex shrink-0 items-center pe-4">
<x-ui.row-actions :actions="$rowActions" :row-label="$rowTitle" />
</div>
@endif
</li>
@endforeach
</ul>
</x-ui.card>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.3.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