Skip to content
Brok UI

Loading…

No results

Issues

Open source

Work tracker issues: one dense row with priority, key, state, labels, estimate, due date and assignee, and a grouped list with a keyboard cursor and selection.

Version
v1.5.2
Stability
stable
License
MIT
Related
Issue List
Todo Item
Item
List Panel
Badge
Avatar
Row Actions
Property Picker

Issue Row — One dense row of a work tracker list: an optional selection checkbox, priority icon, monospace key, workflow state icon, the title as the row's link, label chips folding into +N, an estimate, a due date that turns destructive when overdue, the assignee avatar (or a dashed Unassigned placeholder) and a trailing slot. Static Blade; issue-list adds the keyboard cursor and selection.

Preview

Tone
previews.components.issue-row.default.blade.php Blade
{{-- Rows are list items: put them in a <ul role="list"> (issue-list groups do). --}}
<ul role="list" class="w-full max-w-4xl divide-y divide-border overflow-clip rounded-lg border border-border bg-background">
    <x-ui.issue-row id="ENG-142" key="ENG-142" href="#ENG-142" :title="__('Sign-in fails when the session cookie has expired')" state="started" :progress="0.6" priority="urgent" :labels="[['name' => __('Bug'), 'tone' => 'destructive'], ['name' => __('Auth'), 'tone' => 'info']]" estimate="3" :due="now()->subDays(2)->toDateString()" :assignee="['name' => 'Ada Lovelace']" />
    <x-ui.issue-row id="ENG-139" key="ENG-139" href="#ENG-139" :title="__('Move invoice export to a queued job')" state="review" priority="high" :labels="[['name' => __('Performance'), 'tone' => 'warning']]" estimate="5" :due="now()->toDateString()" :assignee="['name' => 'Grace Hopper']" />
    <x-ui.issue-row id="ENG-150" key="ENG-150" href="#ENG-150" :title="__('Write the upgrade guide for version 3')" state="unstarted" :labels="[['name' => __('Docs'), 'tone' => 'neutral']]" :due="now()->addDays(14)->toDateString()">
        <span>{{ __('3d ago') }}</span>
    </x-ui.issue-row>
</ul>
Null Current
Neutral Current
Muted Current
Info Current
Warning Current
Success Current
Primary Current

Installation

terminal
php artisan ui:add issue-row

Registry contract

php artisan ui:add issue-row 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/issue-row.blade.php
  • blade resources/views/components/ui/issue-row/state.blade.php
  • blade resources/views/components/ui/issue-row/priority.blade.php
Registry dependencies
avatar badge checkbox
Packages
composer: jml/brok:^0.2
npm: alpinejs

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.

issue-row.md
# Brok UI: Issue Row (`issue-row`)

One dense row of a work tracker list: an optional selection checkbox, priority icon, monospace key, workflow state icon, the title as the row's link, label chips folding into +N, an estimate, a due date that turns destructive when overdue, the assignee avatar (or a dashed Unassigned placeholder) and a trailing slot. Static Blade; issue-list adds the keyboard cursor and selection.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add issue-row
```

## Usage

```blade
{{-- Rows are list items: put them in a <ul role="list"> (issue-list groups do). --}}
<ul role="list" class="w-full max-w-4xl divide-y divide-border overflow-clip rounded-lg border border-border bg-background">
    <x-ui.issue-row id="ENG-142" key="ENG-142" href="#ENG-142" :title="__('Sign-in fails when the session cookie has expired')" state="started" :progress="0.6" priority="urgent" :labels="[['name' => __('Bug'), 'tone' => 'destructive'], ['name' => __('Auth'), 'tone' => 'info']]" estimate="3" :due="now()->subDays(2)->toDateString()" :assignee="['name' => 'Ada Lovelace']" />
    <x-ui.issue-row id="ENG-139" key="ENG-139" href="#ENG-139" :title="__('Move invoice export to a queued job')" state="review" priority="high" :labels="[['name' => __('Performance'), 'tone' => 'warning']]" estimate="5" :due="now()->toDateString()" :assignee="['name' => 'Grace Hopper']" />
    <x-ui.issue-row id="ENG-150" key="ENG-150" href="#ENG-150" :title="__('Write the upgrade guide for version 3')" state="unstarted" :labels="[['name' => __('Docs'), 'tone' => 'neutral']]" :due="now()->addDays(14)->toDateString()">
        <span>{{ __('3d ago') }}</span>
    </x-ui.issue-row>
</ul>
```

## Props

- `id` (string|int|null, default `null`) — The issue id used by issue-list for the cursor, selection and events (data-issue-id). Null uses key, then a hash of the title.
- `key` (string|null, default `null`) — The human key shown in monospace, for example ENG-142.
- `title` (string|null, default `null`) — The issue title. It is the row's link text and truncates to one line.
- `href` (string|null, default `null`) — The issue URL. With href the title is an <a>; without it a <button type="button"> (issue-list still dispatches issue-open).
- `state` (triage|backlog|unstarted|started|review|completed|cancelled, default `backlog`) — The workflow state category. It picks the state icon and its token colour; completed and cancelled mute the title and cancelled strikes it through.
- `stateLabel` (string|null, default `null`) — The translated display name of the state (your workflow's own name, for example Ready for QA). Null uses the translated default of the category: Triage, Backlog, Todo, In progress, In review, Done, Cancelled.
- `progress` (float|null, default `null`) — 0 to 1: how much of the started pie is filled. Null draws a half circle. Only used when state is started.
- `stateTone` (neutral|muted|info|warning|success|primary|null, default `null`) — Recolours the state icon. Null keeps the category colour: completed draws a calm success check, review success, started warning, triage info, the rest muted. Pass primary for a brand-coloured Done.
- `priority` (none|urgent|high|medium|low, default `none`) — The priority icon: three dashes, a filled square with an exclamation mark, or signal bars with 3, 2 or 1 filled.
- `priorityLabel` (string|null, default `null`) — The translated display name of the priority. Null uses No priority, Urgent, High, Medium or Low.
- `labels` (array, default `[]`) — Labels as strings or { name, tone } where tone is success|warning|destructive|info|neutral (the badge dot tones). A tone draws a coloured dot; an unknown tone draws none.
- `maxLabels` (int, default `2`) — How many label chips show before the rest fold into a +N chip that lists them in its title and in visually hidden text.
- `estimate` (string|int|null, default `null`) — A small muted estimate chip, for example 3 or 2h.
- `due` (string|DateTimeInterface|null, default `null`) — The due date as an ISO date. It prints in the app locale; the full date is the <time> title and datetime. Before today and not completed or cancelled it is overdue (destructive colour, "Overdue" in the spoken text); today is emphasised.
- `dueFormat` (string, default `MMM D`) — The Carbon isoFormat of the due date in the current year. Other years use the locale's ll format (with the year).
- `assignee` (string|array|null, default `null`) — The assignee name, or { name, src|avatar, initials }. Null draws a dashed placeholder named Unassigned.
- `selectable` (bool, default `false`) — Renders the selection checkbox at the start. Inside a selectable issue-list the row inherits it through @aware.
- `selected` (bool, default `false`) — The first paint of the selection: data-selected and a checked box.
- `focused` (bool, default `false`) — Marks the row as the keyboard cursor (data-focused) for a static render. Inside issue-list the behaviour owns this mark.
- `name` (string|null, default `null`) — The checkbox posts as name[] with the row id as its value. Inside issue-list the row inherits the list's name through @aware.
- `properties` (list<priority|key|state|labels|estimate|due|assignee>|string|null, default `null`) — The display properties to show, like Linear's: a list or a comma-separated string of priority, key, state, labels, estimate, due and assignee. Null shows them all. A hidden part is not rendered and leaves the link description; the title always shows. Inside issue-list the row inherits the list's properties through @aware, so every row lines up.
- `pickers` (array<string, string|array{id: string, value?: mixed}>|string|null, default `null`) — Property triggers. A map of priority, state, labels, estimate, due or assignee to a property-picker id (or { id, value } to send another value, such as a workflow state id), or a string prefix that names "<prefix>-<property>" for every shown property. Each named property renders as its own button that opens that picker beside it. Inside an issue-list the row inherits the list's `pickers` through @aware; a row's own value wins. A named labels, estimate or due property with no value renders a quiet empty-state button (Add label, Set estimate, Set due date) so the value can be added from the row.
- `as` (li|div, default `li`) — The row element. Rows are list items: put them in a <ul role="list"> (issue-list groups do). Use div for a single row outside a list.
- `tone` (null|neutral|muted|info|warning|success|primary, default `null`) — Documented catalog control used by the preview workbench.
- `label` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `showLabel` (bool, default `false`) — Declared by @props in the registry Blade source.
- `announce` (bool, default `true`) — Declared by @props in the registry Blade source.

## Use when

- Use to summarize, sequence, or present data so users can scan it quickly.
- Showing one issue, task or ticket as a dense list row with its key, state, priority, labels, estimate, due date and assignee.
- Building a work tracker list: put rows in issue-list groups for the keyboard cursor, selection and collapsible groups.
- Linking to an issue from another surface (a related-issues card), with as="div" outside a list.

## Avoid when

- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- A personal checklist with strike-through on completion: use <x-ui.todo-item>.
- A generic list row with media, text and actions: use <x-ui.item> or <x-ui.list-panel>.
- Sortable columns, pagination or inline cell editing: use <x-ui.data-table>.
- A board of cards per state: use <x-ui.kanban>.

## Anti-patterns

- Adding display ornament without informational value

## Rules

- Use the `<brok:issue-row>` tag (or `<x-ui.issue-row>`) 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/issue-row
- Registry JSON (files, props, contract): https://brokui.dev/r/open/issue-row.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

long-content.blade.php Blade
{{-- Long titles, many labels, long names and narrow frames. --}}
<div class="flex w-full flex-col gap-6">
    @foreach (['max-w-4xl', 'max-w-md', 'max-w-xs'] as $width)
        <ul role="list" class="w-full {{ $width }} divide-y divide-border overflow-clip rounded-lg border border-border bg-background">
            <x-ui.issue-row id="PLATFORM-10421" key="PLATFORM-10421" href="#PLATFORM-10421" :title="__('Investigate why the nightly synchronisation of supplier price lists intermittently times out for accounts with more than twenty thousand products')" state="started" priority="urgent" :labels="[['name' => __('Integrations'), 'tone' => 'info'], ['name' => __('Customer escalation'), 'tone' => 'destructive'], __('Backend'), __('Suppliers')]" estimate="13" :due="now()->subDays(12)->toDateString()" :assignee="['name' => 'Maximiliane Konstantinopoulou-Schwarzenberger']" selectable />
            <x-ui.issue-row id="PLATFORM-10422" key="PLATFORM-10422" href="#PLATFORM-10422" title="https://example.com/a/very/long/url/that/has/no/spaces/and/must/not/overflow/the/row" state="cancelled" :labels="[__('A label with a very long name that should truncate')]" :due="now()->addYear()->toDateString()" selectable />
        </ul>
    @endforeach
</div>
pickers.blade.php Blade
{{-- Property triggers: with `pickers` each property is its own button above
     the row link. A click, Enter or Space opens the property-picker with that
     id beside it (property-picker-open { id, anchor, value, context }); the
     title still opens the issue. --}}
<div class="flex w-full max-w-4xl flex-col gap-2">
    <ul role="list" class="divide-y divide-border overflow-clip rounded-lg border border-border bg-background">
        <x-ui.issue-row id="ENG-142" key="ENG-142" href="#ENG-142" :title="__('Sign-in fails when the session cookie has expired')" state="started" priority="high" :assignee="['id' => 7, 'name' => 'Ada Lovelace']" :pickers="['state' => 'row-state', 'priority' => 'row-priority', 'assignee' => 'row-assignee']" />
    </ul>

    <x-ui.property-picker id="row-state" detached :label="__('State')">
        @foreach ([['backlog', __('Backlog')], ['started', __('In progress')], ['completed', __('Done')]] as [$value, $name])
            <x-ui.property-picker.option :value="$value" :label="$name">
                <x-slot:icon><x-ui.issue-row.state :state="$value" :label="$name" :announce="false" /></x-slot:icon>
            </x-ui.property-picker.option>
        @endforeach
    </x-ui.property-picker>
    <x-ui.property-picker id="row-priority" detached :label="__('Priority')">
        @foreach ([['urgent', __('Urgent')], ['high', __('High')], ['low', __('Low')]] as [$value, $name])
            <x-ui.property-picker.option :value="$value" :label="$name">
                <x-slot:icon><x-ui.issue-row.priority :priority="$value" :label="$name" /></x-slot:icon>
            </x-ui.property-picker.option>
        @endforeach
    </x-ui.property-picker>
    <x-ui.property-picker id="row-assignee" detached align="end" :label="__('Assignee')" :empty-label="__('Unassigned')">
        <x-ui.property-picker.option value="" :label="__('Unassigned')" />
        @foreach ([[7, 'Ada Lovelace'], [8, 'Grace Hopper']] as [$id, $person])
            <x-ui.property-picker.option :value="$id" :label="$person" />
        @endforeach
    </x-ui.property-picker>
</div>
properties.blade.php Blade
{{-- Display properties: the same issue with every part, then with only key, state, labels and due date (no priority, estimate or assignee). --}}
@php
    $issue = [
        'id' => 'ENG-142',
        'key' => 'ENG-142',
        'href' => '#ENG-142',
        'title' => __('Sign-in fails when the session cookie has expired'),
        'state' => 'started',
        'progress' => 0.6,
        'priority' => 'urgent',
        'labels' => [['name' => __('Bug'), 'tone' => 'destructive'], ['name' => __('Auth'), 'tone' => 'info']],
        'estimate' => '3',
        'due' => now()->addDays(2)->toDateString(),
        'assignee' => ['name' => 'Ada Lovelace'],
    ];
@endphp
<div class="flex w-full max-w-4xl flex-col gap-6">
    <ul role="list" class="divide-y divide-border overflow-clip rounded-lg border border-border bg-background">
        <x-ui.issue-row :attributes="new \Illuminate\View\ComponentAttributeBag($issue)" />
    </ul>
    <ul role="list" class="divide-y divide-border overflow-clip rounded-lg border border-border bg-background">
        <x-ui.issue-row :attributes="new \Illuminate\View\ComponentAttributeBag($issue)" properties="key,state,labels,due" />
    </ul>
</div>
states.blade.php Blade
{{-- Every state and priority, plus selected, cursor, overdue, due today and unassigned. --}}
@php
    $states = [
        ['triage', 'urgent', null],
        ['backlog', 'none', null],
        ['unstarted', 'low', now()->addDays(9)],
        ['started', 'medium', now()],
        ['review', 'high', now()->subDays(3)],
        ['completed', 'high', now()->subDays(3)],
        ['cancelled', 'low', now()->subDays(3)],
    ];
@endphp
<div class="flex w-full max-w-4xl flex-col gap-6">
    <ul role="list" class="divide-y divide-border overflow-clip rounded-lg border border-border bg-background">
        @foreach ($states as $index => [$state, $priority, $due])
            <x-ui.issue-row
                :id="'STATE-'.($index + 1)"
                :key="'STATE-'.($index + 1)"
                :href="'#STATE-'.($index + 1)"
                :title="__('Workflow state: :state', ['state' => $state])"
                :state="$state"
                :priority="$priority"
                :due="$due?->toDateString()"
                :assignee="$index % 2 === 0 ? ['name' => 'Ada Lovelace'] : null"
            />
        @endforeach
    </ul>

    <ul role="list" class="divide-y divide-border overflow-clip rounded-lg border border-border bg-background">
        <x-ui.issue-row id="SEL-1" key="SEL-1" href="#SEL-1" :title="__('Selected row')" state="started" :progress="0.1" priority="medium" selectable selected :assignee="['name' => 'Grace Hopper']" />
        <x-ui.issue-row id="SEL-2" key="SEL-2" href="#SEL-2" :title="__('Keyboard cursor row')" state="started" :progress="0.9" priority="low" selectable focused />
        <x-ui.issue-row id="SEL-3" key="SEL-3" href="#SEL-3" :title="__('Custom state name')" state="review" :state-label="__('Ready for QA')" priority="high" :priority-label="__('Important')" selectable :labels="[['name' => __('QA'), 'tone' => 'success']]" />
        {{-- Done defaults to a calm success check; state-tone="primary" opts into the brand colour. --}}
        <x-ui.issue-row id="SEL-4" key="SEL-4" href="#SEL-4" :title="__('Brand-coloured Done')" state="completed" state-tone="primary" priority="none" selectable />
    </ul>

    <div class="flex flex-wrap items-center gap-4 text-sm">
        @foreach (['triage', 'backlog', 'unstarted', 'started', 'review', 'completed', 'cancelled'] as $state)
            <x-ui.issue-row.state :state="$state" show-label />
        @endforeach
    </div>
    <div class="flex flex-wrap items-center gap-4 text-sm">
        @foreach (['none', 'urgent', 'high', 'medium', 'low'] as $priority)
            <x-ui.issue-row.priority :priority="$priority" show-label />
        @endforeach
    </div>
</div>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
id string | int | null null The issue id used by issue-list for the cursor, selection and events (data-issue-id). Null uses key, then a hash of the title.
key string | null null The human key shown in monospace, for example ENG-142.
title string | null null The issue title. It is the row's link text and truncates to one line.
href string | null null The issue URL. With href the title is an <a>; without it a <button type="button"> (issue-list still dispatches issue-open).
state triage | backlog | unstarted | started | review | completed | cancelled backlog The workflow state category. It picks the state icon and its token colour; completed and cancelled mute the title and cancelled strikes it through.
stateLabel string | null null The translated display name of the state (your workflow's own name, for example Ready for QA). Null uses the translated default of the category: Triage, Backlog, Todo, In progress, In review, Done, Cancelled.
progress float | null null 0 to 1: how much of the started pie is filled. Null draws a half circle. Only used when state is started.
stateTone neutral | muted | info | warning | success | primary | null null Recolours the state icon. Null keeps the category colour: completed draws a calm success check, review success, started warning, triage info, the rest muted. Pass primary for a brand-coloured Done.
priority none | urgent | high | medium | low none The priority icon: three dashes, a filled square with an exclamation mark, or signal bars with 3, 2 or 1 filled.
priorityLabel string | null null The translated display name of the priority. Null uses No priority, Urgent, High, Medium or Low.
labels array [] Labels as strings or { name, tone } where tone is success|warning|destructive|info|neutral (the badge dot tones). A tone draws a coloured dot; an unknown tone draws none.
maxLabels int 2 How many label chips show before the rest fold into a +N chip that lists them in its title and in visually hidden text.
estimate string | int | null null A small muted estimate chip, for example 3 or 2h.
due string | DateTimeInterface | null null The due date as an ISO date. It prints in the app locale; the full date is the <time> title and datetime. Before today and not completed or cancelled it is overdue (destructive colour, "Overdue" in the spoken text); today is emphasised.
dueFormat string MMM D The Carbon isoFormat of the due date in the current year. Other years use the locale's ll format (with the year).
assignee string | array | null null The assignee name, or { name, src|avatar, initials }. Null draws a dashed placeholder named Unassigned.
selectable bool false Renders the selection checkbox at the start. Inside a selectable issue-list the row inherits it through @aware.
selected bool false The first paint of the selection: data-selected and a checked box.
focused bool false Marks the row as the keyboard cursor (data-focused) for a static render. Inside issue-list the behaviour owns this mark.
name string | null null The checkbox posts as name[] with the row id as its value. Inside issue-list the row inherits the list's name through @aware.
properties list<priority | key | state | labels | estimate | due | assignee> | string | null null The display properties to show, like Linear's: a list or a comma-separated string of priority, key, state, labels, estimate, due and assignee. Null shows them all. A hidden part is not rendered and leaves the link description; the title always shows. Inside issue-list the row inherits the list's properties through @aware, so every row lines up.
pickers array<string, string | array{id: string, value?: mixed}> | string | null null Property triggers. A map of priority, state, labels, estimate, due or assignee to a property-picker id (or { id, value } to send another value, such as a workflow state id), or a string prefix that names "<prefix>-<property>" for every shown property. Each named property renders as its own button that opens that picker beside it. Inside an issue-list the row inherits the list's `pickers` through @aware; a row's own value wins. A named labels, estimate or due property with no value renders a quiet empty-state button (Add label, Set estimate, Set due date) so the value can be added from the row.
as li | div li The row element. Rows are list items: put them in a <ul role="list"> (issue-list groups do). Use div for a single row outside a list.
tone null | neutral | muted | info | warning | success | primary null Documented catalog control used by the preview workbench.
label mixed | null null Declared by @props in the registry Blade source.
showLabel bool false Declared by @props in the registry Blade source.
announce bool true Declared by @props in the registry Blade source.

Slots

  • default — Trailing content after the assignee, above the stretched link (for example an updated time or row-actions). Controls here stay separate from the row link.
  • x-ui.issue-row.state — Installed subcomponent from the registry item.
  • x-ui.issue-row.priority — Installed subcomponent from the registry item.

Renders as li, div.

Data slots

Stable hooks for CSS overrides and browser tests.

issue-row issue-row-assignee issue-row-due issue-row-estimate issue-row-key issue-row-labels issue-row-link issue-row-priority issue-row-priority-label issue-row-property issue-row-select issue-row-state issue-row-state-label issue-row-state-progress issue-row-title issue-row-trailing

Behavior

  • The title is the only link. Its ::after covers the row, so a click anywhere opens the issue; the checkbox and the trailing slot sit above it (z-10) and never nest inside the <a>.
  • Sub-components: <x-ui.issue-row.state state progress label show-label announce tone> and <x-ui.issue-row.priority priority label show-label> render the icons alone (a group header, a filter menu, a detail page). Both print "Status: …" or "Priority: …" as visually hidden text unless show-label prints the name.
  • Overdue is computed on the server from now() in the app timezone (Carbon::setTestNow in tests): a due date before today on a row that is not completed or cancelled. data-overdue and data-due=overdue|today|upcoming|closed mark it.
  • Narrow rows drop parts through container queries on the row: labels below 28rem, the estimate below 24rem, the key below 20rem and the due date below 16rem (container content width). The title always keeps the remaining space and truncates; it carries dir="auto", so a title in another script than the page truncates at its own end.
  • States: data-selected (accent ground), data-focused (a lighter accent ground), hover, and the link's focus ring drawn inset on the whole row.
  • Display properties: properties="key,state,labels,due" (or an array) renders only those parts, in their fixed order; unknown names are ignored. A hidden priority or assignee leaves no placeholder, so apps hide parts with a prop instead of arbitrary-variant classes. The list passes one set to every row, which keeps the columns aligned.
  • Property triggers (pickers): each named property is a <button aria-haspopup="dialog"> above the stretched link (z-10), so a click on it never opens the issue. A click, Enter or Space dispatches property-picker-open { id, anchor: the button, value, context: the row id }; the picker opens beside the button and returns focus to it. Values: priority and state as given, labels as their ids (or names), assignee as its id (or name), the due date as Y-m-d, the estimate as given.
  • Focus order: the property buttons follow the visual order (priority, state, title, labels, estimate, due date, assignee). In an issue-list the link keeps the only tab stop per row and ArrowLeft/ArrowRight move through the buttons; outside a list each button is a tab stop.
  • Empty picked properties: with pickers naming labels, estimate or due and no value, the row renders an icon button (a tag, a triangle, a calendar) named "Add label", "Set estimate" or "Set due date" with data-empty="true". It shows on row hover, while focus is in the row, on the issue-list cursor row, while focused or open, and always on devices without hover; it stays in the tab and arrow-key order when hidden.
  • Focus return across a re-render: every property button has a stable id derived from the row id and the property, and the row carries data-anchor-key="issue-row:<id>". When a pick makes Livewire replace the row, the popover focuses the new button with that id, else the row's link, so focus never falls to <body>.
  • Declares registry capability flags: a11y, authoredStateFixtures, 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 one issue, task or ticket as a dense list row with its key, state, priority, labels, estimate, due date and assignee.
  • Building a work tracker list: put rows in issue-list groups for the keyboard cursor, selection and collapsible groups.
  • Linking to an issue from another surface (a related-issues card), with as="div" outside a list.

Avoid when

  • Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
  • A personal checklist with strike-through on completion: use <x-ui.todo-item>.
  • A generic list row with media, text and actions: use <x-ui.item> or <x-ui.list-panel>.
  • Sortable columns, pagination or inline cell editing: use <x-ui.data-table>.
  • A board of cards per state: use <x-ui.kanban>.

Use instead

  • Table for exact comparison
  • Plain text for a single value

Anti-patterns

  • Adding display ornament without informational value
Anatomy
issue-row issue-row-select issue-row-priority issue-row-key issue-row-state issue-row-state-progress issue-row-title issue-row-link issue-row-labels issue-row-estimate issue-row-due issue-row-assignee issue-row-property issue-row-trailing
Theming hooks
badge avatar checkbox

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
native
Focus
native
  • Every icon is aria-hidden with visually hidden text beside it ("Priority: High", "Status: In progress"). The link is described by the key, state, priority, labels, estimate, due date and assignee through aria-describedby, so the cursor move reads the whole row.
  • The assignee is role="img" named "Assignee: name" or "Unassigned". The due date is a <time datetime> whose spoken text says "Overdue, due …" or "Due today": colour is never the only cue.
  • The selection checkbox is named "Select KEY" and sits in a 24 by 24 px target. Logical properties mirror the row under dir="rtl"; the started pie mirrors with it and the key stays left to right.
  • A property button is named by its content ("Priority: High", "Assignee: Ada Lovelace", "Unassigned"), has a 24 px minimum target and a focus ring, and reports aria-expanded while its picker is open.
  • An empty-state property button is named by its action ("Add label", "Set estimate", "Set due date") through aria-label and title; opacity hides it only visually, so a keyboard or screen reader user always reaches it, and focus shows it.
  • 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

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="issue-row-{{ $record->id }}">
    {{-- Rows are list items: put them in a <ul role="list"> (issue-list groups do). --}}
    <ul role="list" class="w-full max-w-4xl divide-y divide-border overflow-clip rounded-lg border border-border bg-background">
        <x-ui.issue-row id="ENG-142" key="ENG-142" href="#ENG-142" :title="__('Sign-in fails when the session cookie has expired')" state="started" :progress="0.6" priority="urgent" :labels="[['name' => __('Bug'), 'tone' => 'destructive'], ['name' => __('Auth'), 'tone' => 'info']]" estimate="3" :due="now()->subDays(2)->toDateString()" :assignee="['name' => 'Ada Lovelace']" />
        <x-ui.issue-row id="ENG-139" key="ENG-139" href="#ENG-139" :title="__('Move invoice export to a queued job')" state="review" priority="high" :labels="[['name' => __('Performance'), 'tone' => 'warning']]" estimate="5" :due="now()->toDateString()" :assignee="['name' => 'Grace Hopper']" />
        <x-ui.issue-row id="ENG-150" key="ENG-150" href="#ENG-150" :title="__('Write the upgrade guide for version 3')" state="unstarted" :labels="[['name' => __('Docs'), 'tone' => 'neutral']]" :due="now()->addDays(14)->toDateString()">
            <span>{{ __('3d ago') }}</span>
        </x-ui.issue-row>
    </ul>
</div>

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/issue-row.blade.php Blade
{{--
    Issue Row: one dense row of a work tracker list.

    Parts from the logical start: an optional selection checkbox, the priority
    icon, the key, the state icon, the title (the row's link), labels, the
    estimate, the due date, the assignee and a trailing slot.

    The title is the only link. Its ::after covers the whole row (the
    "stretched link"), so a click anywhere opens the issue, while the
    checkbox and the trailing slot sit above it and stay separate controls:
    nothing interactive is ever nested inside the <a>.

    `properties` picks which parts show, like Linear's display properties:
    a list (or comma-separated string) of priority, key, state, labels,
    estimate, due and assignee. Null shows them all. A hidden part is not
    rendered and drops out of the link's description; the title always shows.

    Inside an issue list the row inherits `selectable`, `name` and
    `properties` from the list through @aware, so rows written into a group
    need none of them and every row of the list lines up. `pickers` comes
    through @aware too, so a list sets it once; a row's own `pickers` wins.

    `pickers` turns a property into its own button, above the stretched
    link: a click (or Enter or Space) dispatches `property-picker-open`
    { id, anchor, value, context } with the button as the anchor, the
    property's value and the row id, so a property-picker with that id opens
    beside it and returns focus to it. The buttons sit in reading order; in an
    issue list the link keeps the only tab stop and ArrowLeft/ArrowRight
    reach them.

    Each property button has a stable id ("issue-row-<hash of the row id>-
    <property>-button") and the row a `data-anchor-key` ("issue-row:<row
    id>"), so when a pick makes Livewire re-render the row, the popover finds
    the new button (or, failing that, the row's link) and focus stays in the
    row instead of falling to <body>.

    A picked property with no value (labels, estimate, due date) still gets a
    quiet button: an icon named "Add label", "Set estimate" or "Set due date".
    It shows on row hover, while the row holds focus or the list cursor, while
    it is focused or open, and always on a device without hover; it is in the
    tab and arrow-key order at all times. An unassigned row already shows its
    dashed placeholder, so the assignee button is always visible.
--}}
@aware([
    'selectable' => false,
    'name' => null,
    'properties' => null,
    'pickers' => null,
])

@props([
    'id' => null,
    'key' => null,
    'title' => null,
    'href' => null,
    'state' => 'backlog',
    'stateLabel' => null,
    'progress' => null,
    'stateTone' => null,
    'priority' => 'none',
    'priorityLabel' => null,
    'labels' => [],
    'maxLabels' => 2,
    'estimate' => null,
    'due' => null,
    'dueFormat' => 'MMM D',
    'assignee' => null,
    'selectable' => false,
    'selected' => false,
    'focused' => false,
    'name' => null,
    'properties' => null,
    // Property triggers: a map of property => picker id (or ['id' => …,
    // 'value' => …] to send another value, such as a workflow state id), or a
    // string prefix that names "<prefix>-<property>" for every property.
    'pickers' => null,
    'as' => 'li',
])

@php
    $tag = in_array($as, ['li', 'div'], true) ? $as : 'li';
    $title = (string) $title;
    $key = filled($key) ? (string) $key : null;
    $rowId = (string) ($id ?? $key ?? 'issue-'.substr(md5($title), 0, 8));
    $baseId = 'issue-row-'.substr(md5($rowId), 0, 10);
    $closed = in_array($state, ['completed', 'cancelled'], true);

    // Display properties: the parts after the checkbox that may be hidden.
    $allProperties = ['priority', 'key', 'state', 'labels', 'estimate', 'due', 'assignee'];
    $shownProperties = $properties === null
        ? $allProperties
        : array_values(array_intersect($allProperties, is_string($properties)
            ? (preg_split('/[\s,]+/', $properties, -1, PREG_SPLIT_NO_EMPTY) ?: [])
            : array_map('strval', array_filter((array) $properties, 'is_scalar'))));
    $shows = fn (string $property): bool => in_array($property, $shownProperties, true);
    if (! $shows('key')) {
        $key = null;
    }

    // Labels: strings, or { name, tone } where tone is a badge dot tone.
    $tones = ['success', 'warning', 'destructive', 'info', 'neutral'];
    $labelItems = [];
    foreach ((array) $labels as $label) {
        $labelName = is_array($label) ? ($label['name'] ?? $label['label'] ?? null) : $label;
        if (! is_scalar($labelName) || (string) $labelName === '') {
            continue;
        }
        $labelTone = is_array($label) ? ($label['tone'] ?? $label['color'] ?? null) : null;
        $labelItems[] = ['name' => (string) $labelName, 'tone' => in_array($labelTone, $tones, true) ? $labelTone : null, 'id' => is_array($label) ? ($label['id'] ?? null) : null];
    }
    if (! $shows('labels')) {
        $labelItems = [];
    }
    $maxLabels = max(0, (int) $maxLabels);
    $visibleLabels = array_slice($labelItems, 0, $maxLabels);
    $hiddenLabels = array_column(array_slice($labelItems, $maxLabels), 'name');

    // Due date: an ISO date from the server, formatted in the app locale.
    $dueDate = null;
    if (filled($due) && $shows('due')) {
        try {
            $dueDate = \Illuminate\Support\Carbon::parse($due)->locale(app()->getLocale());
        } catch (\Throwable) {
            $dueDate = null;
        }
    }
    $today = now()->startOfDay();
    $dueState = match (true) {
        $dueDate === null => null,
        $closed => 'closed',
        $dueDate->copy()->startOfDay()->lt($today) => 'overdue',
        $dueDate->isSameDay($today) => 'today',
        default => 'upcoming',
    };
    $dueText = $dueDate?->isoFormat($dueDate->year === $today->year ? $dueFormat : 'll');
    $dueFull = $dueDate?->isoFormat('LL');
    $dueSpoken = match ($dueState) {
        'overdue' => __('Overdue, due :date', ['date' => $dueFull]),
        'today' => __('Due today, :date', ['date' => $dueFull]),
        default => __('Due :date', ['date' => $dueFull]),
    };

    // Assignee: a name, or { name, src|avatar, initials }.
    $assigneeName = is_array($assignee) ? ($assignee['name'] ?? null) : $assignee;
    $assigneeName = is_scalar($assigneeName) && (string) $assigneeName !== '' ? (string) $assigneeName : null;
    $assigneeSrc = is_array($assignee) ? ($assignee['src'] ?? $assignee['avatar'] ?? null) : null;
    $assigneeInitials = is_array($assignee) && filled($assignee['initials'] ?? null)
        ? (string) $assignee['initials']
        : ($assigneeName !== null
            ? mb_strtoupper(collect(preg_split('/\s+/u', trim($assigneeName)) ?: [])->filter()->take(2)->map(fn ($part) => mb_substr($part, 0, 1))->implode(''))
            : '');

    $selected = filter_var($selected, FILTER_VALIDATE_BOOLEAN);
    $selectable = filter_var($selectable, FILTER_VALIDATE_BOOLEAN);
    // Property triggers: the picker id and value of each property that opens one.
    $assigneeValue = is_array($assignee) ? ($assignee['id'] ?? $assigneeName) : $assigneeName;
    $pickerValues = [
        'priority' => $priority,
        'state' => $state,
        'labels' => array_map(fn (array $label) => $label['id'] ?? $label['name'], $labelItems),
        'estimate' => filled($estimate) ? $estimate : null,
        'due' => $dueDate?->toDateString(),
        'assignee' => $assigneeValue,
    ];
    $pickerFor = [];
    foreach (array_keys($pickerValues) as $property) {
        $entry = is_string($pickers) && $pickers !== ''
            ? $pickers.'-'.$property
            : (is_array($pickers) ? ($pickers[$property] ?? null) : null);
        $pickerId = is_array($entry) ? ($entry['id'] ?? null) : $entry;
        if (! is_scalar($pickerId) || (string) $pickerId === '' || ! $shows($property)) {
            continue;
        }
        $pickerFor[$property] = [
            'id' => (string) $pickerId,
            'value' => is_array($entry) && array_key_exists('value', $entry) ? $entry['value'] : $pickerValues[$property],
        ];
    }
    // No display utility here: each property adds its own (inline-flex, or
    // hidden with the container query of the part it wraps).
    $pickerButtonId = fn (string $property): string => $baseId.'-'.$property.'-button';
    // An empty picked property: visible on row hover, focus and the list
    // cursor, while focused or open, and always without hover (touch).
    $emptyButton = 'text-muted-foreground opacity-0 group-hover/issue-row:opacity-100 group-focus-within/issue-row:opacity-100 group-data-[focused]/issue-row:opacity-100 focus-visible:opacity-100 aria-expanded:opacity-100 hover:text-foreground [@media(hover:none)]:opacity-100';
    $pickerButton = 'relative z-10 -mx-1 min-h-6 min-w-6 cursor-pointer items-center justify-center rounded-sm px-1 transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring aria-expanded:bg-accent motion-reduce:transition-none';
    // The detail is data: printed through Js::from, the anchor added in the browser.
    $pickerOpen = fn (string $property): string => "\$dispatch('property-picker-open', Object.assign(".\Illuminate\Support\Js::from([
        'id' => $pickerFor[$property]['id'],
        'value' => $pickerFor[$property]['value'],
        'context' => $rowId,
    ])->toHtml().', { anchor: $el }))';

    $describedBy = implode(' ', array_filter([
        $key !== null ? $baseId.'-key' : null,
        $shows('state') ? $baseId.'-state' : null,
        $shows('priority') ? $baseId.'-priority' : null,
        $labelItems !== [] ? $baseId.'-labels' : null,
        filled($estimate) && $shows('estimate') ? $baseId.'-estimate' : null,
        $dueDate !== null ? $baseId.'-due' : null,
        $shows('assignee') ? $baseId.'-assignee' : null,
    ]));
    $linkClasses = 'min-w-0 scroll-mt-12 truncate font-medium outline-none after:absolute after:inset-0 focus-visible:after:ring-[length:var(--ring-width)] focus-visible:after:ring-inset focus-visible:after:ring-ring'
        .($closed ? ' text-muted-foreground' : ' text-foreground')
        .($state === 'cancelled' ? ' line-through' : '');
@endphp

<{{ $tag }}
    data-slot="issue-row"
    data-issue-id="{{ $rowId }}"
    data-state="{{ $state }}"
    data-priority="{{ $priority }}"
    @if ($selected) data-selected="true" @endif
    @if (filter_var($focused, FILTER_VALIDATE_BOOLEAN)) data-focused="true" @endif
    @if ($dueState === 'overdue') data-overdue="true" @endif
    {{ $attributes->merge(['data-anchor-key' => 'issue-row:'.$rowId, 'class' => '@container group/issue-row relative flex min-h-10 min-w-0 items-center gap-2 px-3 py-1 text-sm transition-colors hover:bg-accent/50 data-[focused]:bg-accent/60 data-[selected]:bg-accent']) }}
>
    @if ($selectable)
        {{-- A 24px target around the 16px box (WCAG 2.5.8), above the link. --}}
        <label data-slot="issue-row-select" class="relative z-10 -ms-1 inline-flex size-6 shrink-0 cursor-pointer items-center justify-center">
            <x-ui.checkbox
                :id="$baseId.'-select'"
                :name="filled($name) ? $name.'[]' : null"
                :value="$rowId"
                :checked="$selected"
                :aria-label="__('Select :issue', ['issue' => $key ?? $title])"
            />
        </label>
    @endif

    @if ($shows('priority'))
        @isset($pickerFor['priority'])
        <button type="button" data-slot="issue-row-property" data-property="priority" aria-haspopup="dialog" aria-expanded="false" x-data x-on:click="{{ $pickerOpen('priority') }}" class="{{ $pickerButton }} inline-flex shrink-0" id="{{ $pickerButtonId('priority') }}">
        @endisset
        <x-ui.issue-row.priority id="{{ $baseId }}-priority" :priority="$priority" :label="$priorityLabel" />
        @isset($pickerFor['priority'])
        </button>
        @endisset
    @endif

    @if ($key !== null)
        <span id="{{ $baseId }}-key" data-slot="issue-row-key" class="hidden shrink-0 font-mono text-xs text-muted-foreground tabular-nums @xs:inline" dir="ltr">{{ $key }}</span>
    @endif

    @if ($shows('state'))
        @isset($pickerFor['state'])
        <button type="button" data-slot="issue-row-property" data-property="state" aria-haspopup="dialog" aria-expanded="false" x-data x-on:click="{{ $pickerOpen('state') }}" class="{{ $pickerButton }} inline-flex shrink-0" id="{{ $pickerButtonId('state') }}">
        @endisset
        <x-ui.issue-row.state id="{{ $baseId }}-state" :state="$state" :label="$stateLabel" :progress="$progress" :tone="$stateTone" />
        @isset($pickerFor['state'])
        </button>
        @endisset
    @endif

    {{-- The title takes the remaining space; the link inside keeps its own
         width at the start, and dir="auto" lets a title in another script
         than the page truncate at its own end. --}}
    <span data-slot="issue-row-title" class="flex min-w-0 flex-1">
        @if (filled($href))
            <a href="{{ $href }}" id="{{ $baseId }}-link" dir="auto" data-slot="issue-row-link"@if ($describedBy !== '') aria-describedby="{{ $describedBy }}"@endif class="{{ $linkClasses }}">{{ $title }}</a>
        @else
            <button type="button" id="{{ $baseId }}-link" dir="auto" data-slot="issue-row-link"@if ($describedBy !== '') aria-describedby="{{ $describedBy }}"@endif class="{{ $linkClasses }} text-start">{{ $title }}</button>
        @endif
    </span>

    @if ($labelItems !== [])
        @isset($pickerFor['labels'])
        <button type="button" data-slot="issue-row-property" data-property="labels" aria-haspopup="dialog" aria-expanded="false" x-data x-on:click="{{ $pickerOpen('labels') }}" class="{{ $pickerButton }} hidden min-w-0 shrink @md:inline-flex" id="{{ $pickerButtonId('labels') }}">
        @endisset
        <span id="{{ $baseId }}-labels" data-slot="issue-row-labels" class="hidden min-w-0 shrink items-center justify-end gap-1 overflow-hidden @md:flex">
            <span class="sr-only">{{ __('Labels:') }}</span>
            @foreach ($visibleLabels as $label)
                <x-ui.badge variant="outline" size="sm" :dot="$label['tone'] ?? false" class="min-w-0" :title="$label['name']">{{ $label['name'] }}</x-ui.badge>
            @endforeach
            @if ($hiddenLabels !== [])
                <x-ui.badge variant="outline" size="sm" class="shrink-0 tabular-nums" :title="implode(', ', $hiddenLabels)">
                    <span aria-hidden="true">+{{ count($hiddenLabels) }}</span>
                    <span class="sr-only">{{ trans_choice(':count more: :items|:count more: :items', count($hiddenLabels), ['count' => count($hiddenLabels), 'items' => implode(', ', $hiddenLabels)]) }}</span>
                </x-ui.badge>
            @endif
        </span>
        @isset($pickerFor['labels'])
        </button>
        @endisset
    @elseif (isset($pickerFor['labels']))
        <button type="button" data-slot="issue-row-property" data-property="labels" data-empty="true" aria-haspopup="dialog" aria-expanded="false" aria-label="{{ __('Add label') }}" title="{{ __('Add label') }}" x-data x-on:click="{{ $pickerOpen('labels') }}" class="{{ $pickerButton }} {{ $emptyButton }} hidden shrink-0 @md:inline-flex" id="{{ $pickerButtonId('labels') }}">
            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" aria-hidden="true" focusable="false" class="size-4">
                <path d="M2.5 2.5h5l6 6-5 5-6-6z" />
                <circle cx="5.5" cy="5.5" r="1" fill="currentColor" stroke="none" />
            </svg>
        </button>
    @endif

    @if (filled($estimate) && $shows('estimate'))
        @isset($pickerFor['estimate'])
        <button type="button" data-slot="issue-row-property" data-property="estimate" aria-haspopup="dialog" aria-expanded="false" x-data x-on:click="{{ $pickerOpen('estimate') }}" class="{{ $pickerButton }} hidden shrink-0 @sm:inline-flex" id="{{ $pickerButtonId('estimate') }}">
        @endisset
        <span id="{{ $baseId }}-estimate" data-slot="issue-row-estimate" class="hidden shrink-0 @sm:inline-flex">
            <x-ui.badge variant="secondary" size="sm" class="tabular-nums"><span class="sr-only">{{ __('Estimate:') }}</span> {{ $estimate }}</x-ui.badge>
        </span>
        @isset($pickerFor['estimate'])
        </button>
        @endisset
    @elseif (isset($pickerFor['estimate']))
        <button type="button" data-slot="issue-row-property" data-property="estimate" data-empty="true" aria-haspopup="dialog" aria-expanded="false" aria-label="{{ __('Set estimate') }}" title="{{ __('Set estimate') }}" x-data x-on:click="{{ $pickerOpen('estimate') }}" class="{{ $pickerButton }} {{ $emptyButton }} hidden shrink-0 @sm:inline-flex" id="{{ $pickerButtonId('estimate') }}">
            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="size-4">
                <path d="M8 2.5 13.5 12.5h-11z" />
            </svg>
        </button>
    @endif

    @if ($dueDate !== null)
        @isset($pickerFor['due'])
        <button type="button" data-slot="issue-row-property" data-property="due" aria-haspopup="dialog" aria-expanded="false" x-data x-on:click="{{ $pickerOpen('due') }}" class="{{ $pickerButton }} hidden shrink-0 @3xs:inline-flex" id="{{ $pickerButtonId('due') }}">
        @endisset
        <time
            id="{{ $baseId }}-due"
            data-slot="issue-row-due"
            data-due="{{ $dueState }}"
            datetime="{{ $dueDate->toDateString() }}"
            title="{{ $dueFull }}"
            @class([
                'hidden shrink-0 whitespace-nowrap text-xs tabular-nums @3xs:inline',
                'font-medium text-destructive' => $dueState === 'overdue',
                'font-medium text-foreground' => $dueState === 'today',
                'text-muted-foreground' => ! in_array($dueState, ['overdue', 'today'], true),
            ])
        ><span aria-hidden="true">{{ $dueText }}</span><span class="sr-only">{{ $dueSpoken }}</span></time>
        @isset($pickerFor['due'])
        </button>
        @endisset
    @elseif (isset($pickerFor['due']))
        <button type="button" data-slot="issue-row-property" data-property="due" data-empty="true" aria-haspopup="dialog" aria-expanded="false" aria-label="{{ __('Set due date') }}" title="{{ __('Set due date') }}" x-data x-on:click="{{ $pickerOpen('due') }}" class="{{ $pickerButton }} {{ $emptyButton }} hidden shrink-0 @3xs:inline-flex" id="{{ $pickerButtonId('due') }}">
            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="size-4">
                <rect x="2.5" y="3.5" width="11" height="10" rx="1.5" />
                <path d="M2.5 6.5h11M5.5 2v3M10.5 2v3" />
            </svg>
        </button>
    @endif

    @if ($shows('assignee'))
        @isset($pickerFor['assignee'])
        <button type="button" data-slot="issue-row-property" data-property="assignee" aria-haspopup="dialog" aria-expanded="false" x-data x-on:click="{{ $pickerOpen('assignee') }}" class="{{ $pickerButton }} inline-flex shrink-0" id="{{ $pickerButtonId('assignee') }}">
        @endisset
    @endif
    @if ($shows('assignee') && $assigneeName !== null)
        <span id="{{ $baseId }}-assignee" data-slot="issue-row-assignee" role="img" aria-label="{{ __('Assignee: :name', ['name' => $assigneeName]) }}" title="{{ $assigneeName }}" class="inline-flex shrink-0">
            <x-ui.avatar size="xs" :src="$assigneeSrc" alt="" :fallback="$assigneeInitials" aria-hidden="true" />
        </span>
    @elseif ($shows('assignee'))
        <span id="{{ $baseId }}-assignee" data-slot="issue-row-assignee" data-unassigned="true" role="img" aria-label="{{ __('Unassigned') }}" title="{{ __('Unassigned') }}" class="inline-flex size-6 shrink-0 items-center justify-center rounded-full border border-dashed border-muted-foreground text-muted-foreground">
            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" focusable="false" class="size-3">
                <circle cx="8" cy="5.5" r="2.5" />
                <path d="M3.5 13.5c.6-2.3 2.4-3.5 4.5-3.5s3.9 1.2 4.5 3.5" stroke-linecap="round" />
            </svg>
        </span>
    @endif
    @if ($shows('assignee'))
        @isset($pickerFor['assignee'])
        </button>
        @endisset
    @endif

    @if (! $slot->isEmpty())
        <span data-slot="issue-row-trailing" class="relative z-10 inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground">{{ $slot }}</span>
    @endif
</{{ $tag }}>
resources/views/components/ui/issue-row/state.blade.php Blade
@props([
    // triage | backlog | unstarted | started | review | completed | cancelled
    'state' => 'backlog',
    // The translated display name of the workflow state ("In progress").
    // Null uses the translated default for the category.
    'label' => null,
    // 0 to 1: how far a started item is. Null draws the half circle.
    'progress' => null,
    // true: print the name beside the icon. false: the name is visually hidden.
    'showLabel' => false,
    // false drops the hidden "Status: …" text, for a caller that names the
    // state itself (a group header prints the state name next to the icon).
    'announce' => true,
    // Null keeps the category colour (completed draws a calm success check).
    // neutral | muted | info | warning | success | primary recolours the icon,
    // for example tone="primary" for a brand-coloured Done.
    'tone' => null,
])

@php
    $states = ['triage', 'backlog', 'unstarted', 'started', 'review', 'completed', 'cancelled'];
    $state = in_array($state, $states, true) ? $state : 'backlog';
    $label = filled($label) ? (string) $label : match ($state) {
        'triage' => __('Triage'),
        'backlog' => __('Backlog'),
        'unstarted' => __('Todo'),
        'started' => __('In progress'),
        'review' => __('In review'),
        'completed' => __('Done'),
        'cancelled' => __('Cancelled'),
    };
    // Full class strings per state so the JIT sees every utility.
    $toneClass = match ($state) {
        'triage' => 'text-info',
        'started' => 'text-warning',
        'review' => 'text-success',
        'completed' => 'text-success',
        default => 'text-muted-foreground',
    };
    $tones = [
        'neutral' => 'text-foreground',
        'muted' => 'text-muted-foreground',
        'info' => 'text-info',
        'warning' => 'text-warning',
        'success' => 'text-success',
        'primary' => 'text-primary',
    ];
    $toneClass = $tones[$tone] ?? $toneClass;
    // The pie of a started item fills a circle of radius 2.5 with a stroke as
    // wide as its diameter: the dash length is the share of the circumference.
    $fraction = $progress === null || $progress === '' ? 0.5 : max(0.0, min(1.0, (float) $progress));
    $circumference = 2 * M_PI * 2.5;
    $dash = round($fraction * $circumference, 3);
@endphp

<span
    data-slot="issue-row-state"
    data-state="{{ $state }}"
    {{ $attributes->merge(['class' => 'inline-flex shrink-0 items-center gap-2']) }}
>
    {{-- The pie sweeps in the reading direction, so it mirrors under dir="rtl";
         the other glyphs are symmetric. --}}
    <svg
        viewBox="0 0 14 14"
        fill="none"
        aria-hidden="true"
        focusable="false"
        @class(['size-4 shrink-0', $toneClass, 'rtl:-scale-x-100' => $state === 'started'])
    >
        @switch($state)
            @case('triage')
                <circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.5" />
                <path d="M4.5 5.5h5M8 4l1.5 1.5L8 7M9.5 8.5h-5M6 7 4.5 8.5 6 10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
                @break
            @case('backlog')
                <circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.5" stroke-dasharray="1.4 1.74" />
                @break
            @case('unstarted')
                <circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.5" />
                @break
            @case('started')
                <circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.5" />
                @if ($dash > 0)
                    <circle data-slot="issue-row-state-progress" cx="7" cy="7" r="2.5" stroke="currentColor" stroke-width="5" stroke-dasharray="{{ $dash }} {{ round($circumference, 3) }}" transform="rotate(-90 7 7)" />
                @endif
                @break
            @case('review')
                <circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.5" />
                <circle cx="7" cy="7" r="2.5" fill="currentColor" />
                @break
            @case('completed')
                <circle cx="7" cy="7" r="7" fill="currentColor" />
                <path d="m4.25 7.25 1.9 1.9 3.6-4" class="stroke-background" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
                @break
            @case('cancelled')
                <circle cx="7" cy="7" r="7" fill="currentColor" />
                <path d="m4.75 4.75 4.5 4.5m0-4.5-4.5 4.5" class="stroke-background" stroke-width="1.5" stroke-linecap="round" />
                @break
        @endswitch
    </svg>
    @if ($showLabel)
        <span data-slot="issue-row-state-label" class="min-w-0 truncate">{{ $label }}</span>
    @elseif ($announce)
        <span class="sr-only">{{ __('Status: :state', ['state' => $label]) }}</span>
    @endif
</span>
resources/views/components/ui/issue-row/priority.blade.php Blade
@props([
    // none | urgent | high | medium | low
    'priority' => 'none',
    // The translated display name. Null uses the translated default.
    'label' => null,
    // true: print the name beside the icon. false: the name is visually hidden.
    'showLabel' => false,
])

@php
    $priorities = ['none', 'urgent', 'high', 'medium', 'low'];
    $priority = in_array($priority, $priorities, true) ? $priority : 'none';
    $label = filled($label) ? (string) $label : match ($priority) {
        'none' => __('No priority'),
        'urgent' => __('Urgent'),
        'high' => __('High'),
        'medium' => __('Medium'),
        'low' => __('Low'),
    };
    // Signal bars: how many of the three bars are filled.
    $filled = match ($priority) {
        'high' => 3,
        'medium' => 2,
        'low' => 1,
        default => 0,
    };
    $tone = match ($priority) {
        'urgent' => 'text-destructive',
        'none' => 'text-muted-foreground',
        default => 'text-foreground',
    };
@endphp

<span
    data-slot="issue-row-priority"
    data-priority="{{ $priority }}"
    {{ $attributes->merge(['class' => 'inline-flex shrink-0 items-center gap-2']) }}
>
    <svg viewBox="0 0 14 14" fill="currentColor" aria-hidden="true" focusable="false" class="size-4 shrink-0 {{ $tone }}">
        @if ($priority === 'none')
            <rect x="1" y="6.25" width="3" height="1.5" rx="0.5" opacity="0.7" />
            <rect x="5.5" y="6.25" width="3" height="1.5" rx="0.5" opacity="0.7" />
            <rect x="10" y="6.25" width="3" height="1.5" rx="0.5" opacity="0.7" />
        @elseif ($priority === 'urgent')
            <rect x="1" y="1" width="12" height="12" rx="3" />
            <rect x="6.25" y="3.5" width="1.5" height="4.5" rx="0.75" class="fill-background" />
            <rect x="6.25" y="9.25" width="1.5" height="1.5" rx="0.75" class="fill-background" />
        @else
            @foreach ([[1.5, 8, 4], [5.5, 5, 7], [9.5, 2, 10]] as $index => [$x, $y, $height])
                <rect x="{{ $x }}" y="{{ $y }}" width="3" height="{{ $height }}" rx="1" @if ($index >= $filled) opacity="0.25" @endif />
            @endforeach
        @endif
    </svg>
    @if ($showLabel)
        <span data-slot="issue-row-priority-label" class="min-w-0 truncate">{{ $label }}</span>
    @else
        <span class="sr-only">{{ __('Priority: :priority', ['priority' => $label]) }}</span>
    @endif
</span>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.5.2
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