Skip to content
Brok UI

Loading…

No results

List Panel

Open source

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.

Version
v1.3.0
Stability
stable
License
MIT
Related
Animated List
Check List
Fact List

Preview

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
previews.components.list-panel.default.blade.php 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>

Installation

terminal
php artisan ui:add list-panel

Registry contract

php artisan ui:add list-panel 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/list-panel.blade.php
Registry dependencies
avatar badge card row-actions
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.

list-panel.md
# 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.blade.php Blade
{{-- `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

manifest knowledge + registry-derived coverage

Props

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

list-panel list-panel-actions list-panel-item list-panel-list

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

Scannable data display

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
root list-panel-list list-panel-item list-panel-actions
Theming hooks
list-panel list-panel-actions list-panel-item list-panel-list

Accessibility

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

Source

The exact, editable file ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/list-panel.blade.php Blade
@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