Row Actions
The eligible quick action(s) for a table row inline, plus an overflow menu for the rest, driven by an actions array.
Preview
<x-ui.row-actions
row-label="order #1042"
:actions="[
['key' => 'release', 'label' => __('Release'), 'kind' => 'submit', 'url' => '#', 'quick' => true],
['key' => 'view', 'label' => __('View order'), 'kind' => 'link', 'url' => '#'],
['key' => 'invoice', 'label' => __('Download invoice'), 'kind' => 'link', 'url' => '#'],
['key' => 'refund', 'label' => __('Refund'), 'kind' => 'dialog'],
]"
/>
Installation
php artisan ui:add row-actions
Note
This component ships an Alpine behavior module at
resources/js/ui/row-actions.js. Import it once from your bundle so it registers on alpine:init:
import './row-actions.js';
Registry contract
php artisan ui:add row-actions
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/row-actions.blade.php
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: Row Actions (`row-actions`)
The eligible quick action(s) for a table row inline, plus an overflow menu for the rest, driven by an actions array.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add row-actions
```
## Usage
```blade
<x-ui.row-actions
row-label="order #1042"
:actions="[
['key' => 'release', 'label' => __('Release'), 'kind' => 'submit', 'url' => '#', 'quick' => true],
['key' => 'view', 'label' => __('View order'), 'kind' => 'link', 'url' => '#'],
['key' => 'invoice', 'label' => __('Download invoice'), 'kind' => 'link', 'url' => '#'],
['key' => 'refund', 'label' => __('Refund'), 'kind' => 'dialog'],
]"
/>
```
## Props
- `actions` (array, default `[]`) — Eligible actions: each entry is ['key','label','icon','kind' => link|submit|wire|dialog,'url','newTab','quick','method','fields','wire','params','confirm','variant']. Only entries with quick=true render inline; the rest fall into the overflow menu. submit posts a real form: method POST (default), PUT, PATCH or DELETE (spoofed with CSRF through the form item) with fields as hidden inputs. wire (or a wire key) calls that Livewire method with params. confirm (true, a title, or ['title','description','label','cancel','busyLabel']) adds an alert-dialog step to a submit or wire action. variant => destructive paints the overflow item and the confirm button destructive. icon, when present, is raw SVG path-data rendered before the label (see the TRUST BOUNDARY note in the Blade source) — a label-only action renders unchanged.
- `rowLabel` (string, default ``) — Identifies the row for the accessible name, e.g. "order #1042", so a screen reader does not announce the same action label identically on every row.
- `anchored` (bool, default `true`) — Forwards to the overflow dropdown's anchored/teleported placement, so a scrollable or overflow-clipping table row never cuts the menu off.
- `moreLabel` (string|null, default `null`) — Accessible name for the overflow trigger. Defaults to a localized "More actions for :row" (or "More actions" without rowLabel).
- `display` (both|label|icon, default `both`) — How a quick action shows: icon + label, text only, or an icon-only square with the name in its accessible label and title.
## Use when
- Use to summarize, sequence, or present data so users can scan it quickly.
- A table row has one state-driven next action worth surfacing inline, plus other actions that belong behind a menu.
- Every action already went through the row's own eligibility/permission check, so an absent action here is never merely hidden.
## Avoid when
- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- None of the row's actions are eligible; render nothing rather than an empty control.
- The same action set applies to a whole selection rather than one row — use a bulk action bar instead.
## Anti-patterns
- Adding display ornament without informational value
## Rules
- Use the `<brok:row-actions>` tag (or `<x-ui.row-actions>`) 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/row-actions
- Registry JSON (files, props, contract): https://brokui.dev/r/open/row-actions.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
{{-- A confirm step before a destructive action: the quick Disconnect posts a
DELETE through a native alert dialog (no JavaScript), and the overflow
Remove does the same from the menu. Archive is a Livewire action
(`wire`): its confirm is the Alpine alert dialog, which calls the method
inside a Livewire component (here, without Livewire, it only closes). --}}
<x-ui.row-actions
id="connection-1042"
row-label="Claude Desktop"
:actions="[
['key' => 'disconnect', 'label' => __('Disconnect'), 'kind' => 'submit', 'url' => '#', 'method' => 'DELETE', 'quick' => true, 'variant' => 'destructive', 'confirm' => ['title' => __('Disconnect Claude Desktop?'), 'description' => __('The client loses access at once and its refresh tokens stop working.')]],
['key' => 'details', 'label' => __('View details'), 'kind' => 'link', 'url' => '#'],
['key' => 'archive', 'label' => __('Archive'), 'wire' => 'archive', 'params' => [1042], 'confirm' => ['description' => __('Archived clients keep their history and can be restored.')]],
['key' => 'remove', 'label' => __('Remove'), 'kind' => 'submit', 'url' => '#', 'method' => 'DELETE', 'variant' => 'destructive', 'confirm' => true],
]"
/>
{{-- `display`: label (text only), both (icon + label), icon (icon-only squares,
the name in the accessible label and title). --}}
@php
$actions = [
['key' => 'edit', 'label' => __('Edit'), 'icon' => '<path d=\'M12 20h9\' /><path d=\'M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\' />', 'kind' => 'link', 'url' => '#', 'quick' => true],
['key' => 'duplicate', 'label' => __('Duplicate'), 'icon' => '<rect x=\'9\' y=\'9\' width=\'13\' height=\'13\' rx=\'2\' /><path d=\'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\' />', 'kind' => 'dialog', 'quick' => true],
['key' => 'archive', 'label' => __('Archive'), 'icon' => '<rect x=\'2\' y=\'3\' width=\'20\' height=\'5\' rx=\'1\' /><path d=\'M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8M10 12h4\' />', 'kind' => 'dialog'],
['key' => 'delete', 'label' => __('Delete'), 'icon' => '<path d=\'M3 6h18\' /><path d=\'M8 6V4h8v2\' /><path d=\'M19 6l-1 14H6L5 6\' />', 'kind' => 'dialog'],
];
@endphp
<div class="flex flex-col items-end gap-4">
<x-ui.row-actions row-label="invoice #1042" display="label" :actions="$actions" />
<x-ui.row-actions row-label="invoice #1042" display="both" :actions="$actions" />
<x-ui.row-actions row-label="invoice #1042" display="icon" :actions="$actions" />
</div>
{{-- icon is raw, developer-authored SVG path data (see the TRUST BOUNDARY
note in the component source) — never bind it to CMS/user input. --}}
<x-ui.row-actions
row-label="order #1042"
:actions="[
[
'key' => 'release',
'label' => __('Release'),
'icon' => '<path d=\'M5 12h14M13 6l6 6-6 6\' />',
'kind' => 'submit',
'url' => '#',
'quick' => true,
],
[
'key' => 'view',
'label' => __('View order'),
'icon' => '<path d=\'M2.5 12s3.5-7 9.5-7 9.5 7 9.5 7-3.5 7-9.5 7-9.5-7-9.5-7Z\' /><circle cx=\'12\' cy=\'12\' r=\'3\' />',
'kind' => 'link',
'url' => '#',
],
['key' => 'invoice', 'label' => __('Download invoice'), 'kind' => 'link', 'url' => '#'],
[
'key' => 'refund',
'label' => __('Refund'),
'icon' => '<path d=\'M3 12a9 9 0 1 0 3-6.7\' /><path d=\'M3 4v5h5\' />',
'kind' => 'dialog',
],
]"
/>
Long Content
<div class="max-w-xs">
<x-ui.row-actions
row-label="order #90142-B"
:actions="[
[
'key' => 'release',
'label' => __('Release for fulfilment'),
'kind' => 'submit',
'url' => '#',
'quick' => true,
],
['key' => 'view', 'label' => __('View order details'), 'kind' => 'link', 'url' => '#'],
['key' => 'invoice', 'label' => __('Download invoice PDF'), 'kind' => 'link', 'url' => '#'],
['key' => 'notes', 'label' => __('Add an internal note'), 'kind' => 'dialog'],
['key' => 'refund', 'label' => __('Issue a partial refund'), 'kind' => 'dialog'],
]"
/>
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| actions | array | [] | Eligible actions: each entry is ['key','label','icon','kind' => link|submit|wire|dialog,'url','newTab','quick','method','fields','wire','params','confirm','variant']. Only entries with quick=true render inline; the rest fall into the overflow menu. submit posts a real form: method POST (default), PUT, PATCH or DELETE (spoofed with CSRF through the form item) with fields as hidden inputs. wire (or a wire key) calls that Livewire method with params. confirm (true, a title, or ['title','description','label','cancel','busyLabel']) adds an alert-dialog step to a submit or wire action. variant => destructive paints the overflow item and the confirm button destructive. icon, when present, is raw SVG path-data rendered before the label (see the TRUST BOUNDARY note in the Blade source) — a label-only action renders unchanged. |
| rowLabel | string | Identifies the row for the accessible name, e.g. "order #1042", so a screen reader does not announce the same action label identically on every row. | |
| anchored | bool | true | Forwards to the overflow dropdown's anchored/teleported placement, so a scrollable or overflow-clipping table row never cuts the menu off. |
| moreLabel | string | null | null | Accessible name for the overflow trigger. Defaults to a localized "More actions for :row" (or "More actions" without rowLabel). |
| display | both | label | icon | both | How a quick action shows: icon + label, text only, or an icon-only square with the name in its accessible label and title. |
Slots
Default Blade slot only.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Renders nothing when the actions array is empty.
- An action's icon, when present, renders before its label in both the quick control and the overflow menu item; without one, the action renders exactly as it always has.
- link actions are plain anchors; submit actions post a real form with CSRF; dialog actions (and any overflow action that is not a link) dispatch a row-actions:action browser event for the host to act on.
- The overflow menu's keyboard navigation, roving focus, and Escape/outside-click handling come from the composed dropdown.
- Dispatches row-actions:action from the component root rather than from inside the teleported overflow menu, so a host that wraps the component receives it from both quick and overflow actions.
- A submit action with confirm opens a native alert dialog (a <dialog> opened by an invoker command) whose action posts the real form: no JavaScript and no inline script, so it works under a strict Content-Security-Policy. It needs invoker-command support (Chrome 135, Firefox 144, Safari 26.2).
- A wire action calls the Livewire method on the component that renders the row (wire:click with JSON-encoded params); the quick button shows its loading state through wire-target. With confirm it opens the Alpine alert dialog, which calls the method, stays busy until it returns, then closes and returns focus to the trigger.
- A submit action with method PUT, PATCH or DELETE, or with fields, renders through the form item (CSRF, spoofed _method, hidden inputs); a plain POST without fields keeps its original markup.
- An overflow submit action without confirm still dispatches row-actions:action, as before. An overflow action with confirm opens its dialog after the menu has closed, so focus returns to the menu trigger.
- Each confirm dialog is named after the component id and the action key, so give the component an id (or keep the generated one) and give every action a unique key.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, authoredStateFixtures, responsive, rtl, darkMode, localized.
Guidance
Present data for rapid scanning.
Use when
- Use to summarize, sequence, or present data so users can scan it quickly.
- A table row has one state-driven next action worth surfacing inline, plus other actions that belong behind a menu.
- Every action already went through the row's own eligibility/permission check, so an absent action here is never merely hidden.
Avoid when
- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- None of the row's actions are eligible; render nothing rather than an empty control.
- The same action set applies to a whole selection rather than one row — use a bulk action bar 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
- managed
- Focus
none
- Meet the WCAG 2.2 AA target declared in meta.a11y.
- Every action's accessible name restates which row it acts on.
- 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
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="row-actions-{{ $record->id }}">
<x-ui.row-actions
row-label="order #1042"
:actions="[
['key' => 'release', 'label' => __('Release'), 'kind' => 'submit', 'url' => '#', 'quick' => true],
['key' => 'view', 'label' => __('View order'), 'kind' => 'link', 'url' => '#'],
['key' => 'invoice', 'label' => __('Download invoice'), 'kind' => 'link', 'url' => '#'],
['key' => 'refund', 'label' => __('Refund'), 'kind' => 'dialog'],
]"
/>
</div>
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
{{--
Row Actions — the eligible quick action(s) for a table row, named, plus
an overflow menu for the rest.
Which action gets a `quick` slot is decided by the data, not by this
component: only mark `quick` the one action a row's own state actually
calls for, so a row offers a control when it needs one and none when it
does not. Everything else — secondary and less time-critical actions —
lives behind the overflow menu.
The quick action is LABELLED, never a bare icon: a 40px icon square with
the name only in a `title` makes an operator hover to learn what it does,
and makes two different row actions look identical at a glance.
The accessible name repeats what row it acts on (`rowLabel`): a screen
reader reads this column row after row with no sight of which row it is
in, so the visible label alone ("Release") would announce identically on
every row.
Generalised from `orders/row-actions.blade.php` (+ its sibling
`orders/action-menu.blade.php`) and `inventory/row-menu.blade.php`. The
domain-specific action set (release/pack/refund/adjust-stock/…) and the
Orders/Inventory read-model eligibility rules are left out on purpose —
callers already filter `actions` down to what is permitted and valid
before it reaches this component; an absent action here means "not
permitted", never merely hidden.
`anchored` forwards to the overflow <x-ui.dropdown>: table rows routinely
sit inside a `overflow-hidden`/scrollable ancestor that would otherwise
clip the panel, so this defaults to true (see dropdown's own `anchored`
prop for the teleport + measured placement it buys).
Confirm, verbs and Livewire (all opt-in per action):
- `confirm` (true, a title string, or ['title', 'description', 'label',
'cancel', 'busyLabel']) puts an alert dialog between the control and a
`submit` or `wire` action. A `submit` confirm is the native alert dialog
(a <dialog> opened by an invoker command that posts the real form: no
JavaScript, works under a strict CSP); a `wire` confirm is the Alpine
alert dialog that calls the Livewire method and stays busy until it
returns. `link` and `dialog` actions ignore it.
- `method` (POST default, PUT, PATCH or DELETE) and `fields` (hidden
inputs as ['name' => 'value']) shape a `submit` action's form; the form
item adds CSRF and the spoofed _method field.
- kind `wire` (or a `wire` key) calls a Livewire method on the component
that renders this row: `wire` is the method name, `params` its
arguments. The quick control shows the button's loading state.
- `variant` => 'destructive' paints the overflow item and the confirm
button as destructive.
An overflow `submit` action without `confirm` still dispatches the
`row-actions:action` event, as before.
TRUST BOUNDARY: an action's `icon`, when present, is rendered as raw SVG
markup ({!! !!}) inside a fixed wrapper — it must be developer-authored
path data, never user/CMS input. An action with no `icon` renders exactly
as it did before this existed: a label-only control.
--}}
@props([
// [['key', 'label', 'icon' => null, 'kind' => 'link', 'url' => null, 'newTab' => false, 'quick' => false,
// 'method' => 'POST', 'fields' => [], 'wire' => null, 'params' => [], 'confirm' => null, 'variant' => 'default']]
// kind: link (default, plain navigation) | submit (a form post) | wire (a
// Livewire method call) | dialog (dispatches a `row-actions:action` event
// for the host to open a confirm/drawer).
// icon: optional raw SVG path-data string rendered before the label — see
// the TRUST BOUNDARY note above.
'actions' => [],
// Identifies the row for the accessible name, e.g. "order #1042".
'rowLabel' => '',
'anchored' => true,
'moreLabel' => null,
// How a quick action shows: both (icon + label, the default), label (text
// only), icon (an icon-only square whose name lives in the accessible
// label and title — for a dense table where every row shares one action).
'display' => 'both',
])
@php
$actions = array_values(array_filter((array) $actions, 'is_array'));
// A stable id to dispatch FROM (see below); also the base of each confirm
// dialog's name, so two rows never share a dialog.
$instanceId = (string) ($attributes->get('id') ?: 'row-actions-'.\Illuminate\Support\Str::random(8));
// Fill in the opt-in keys once, so the markup below reads plain flags.
$actions = array_map(static function (array $action, int $index) use ($instanceId, $rowLabel): array {
$wire = trim((string) ($action['wire'] ?? ''));
$kind = (string) ($action['kind'] ?? ($wire !== '' ? 'wire' : 'link'));
// Only a plain method name becomes a wire:click call; anything else
// falls back to the host event instead of printing an expression.
if ($kind === 'wire' && preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $wire) !== 1) {
$kind = 'dialog';
}
$params = array_values((array) ($action['params'] ?? []));
$method = strtoupper((string) ($action['method'] ?? 'POST'));
$label = (string) ($action['label'] ?? '');
$confirm = $action['confirm'] ?? null;
$confirmation = null;
if (in_array($kind, ['submit', 'wire'], true) && $confirm !== null && $confirm !== false && $confirm !== '') {
$options = is_array($confirm) ? $confirm : (is_string($confirm) ? ['title' => $confirm] : []);
// The index keeps two keys with the same slug (delete, Delete) apart.
$slug = trim(\Illuminate\Support\Str::slug((string) ($action['key'] ?? '')).'-'.$index, '-');
$name = $instanceId.'-'.$slug;
$confirmation = [
'native' => $kind === 'submit',
'slug' => $slug,
'name' => $name,
// The id the alert dialog gives its native <dialog>.
'dialogId' => 'alert-dialog-'.\Illuminate\Support\Str::slug($name),
'title' => (string) ($options['title'] ?? ($rowLabel !== ''
? __(':action :row?', ['action' => $label, 'row' => $rowLabel])
: __(':action?', ['action' => $label]))),
'description' => (string) ($options['description'] ?? __('Choose :action to continue, or Cancel to go back.', ['action' => $label])),
'label' => (string) ($options['label'] ?? $label),
'cancel' => (string) ($options['cancel'] ?? __('Cancel')),
'busyLabel' => $options['busyLabel'] ?? null,
];
}
return [
...$action,
// The caller's own entry is the row-actions:action event detail.
'original' => $action,
'kind' => $kind,
'label' => $label,
'method' => in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true) ? $method : 'POST',
'fields' => (array) ($action['fields'] ?? []),
'wire' => $kind === 'wire' ? $wire : null,
'params' => $params,
// The wire:click expression: the method and its JSON-encoded arguments.
'wireCall' => $kind === 'wire'
? $wire.'('.implode(', ', array_map(static fn (mixed $param): string => (string) \Illuminate\Support\Js::from($param), $params)).')'
: null,
'destructive' => ($action['variant'] ?? null) === 'destructive',
'confirmation' => $confirmation,
];
}, $actions, array_keys($actions));
$quick = array_values(array_filter($actions, static fn (array $a): bool => (bool) ($a['quick'] ?? false)));
$overflow = array_values(array_filter($actions, static fn (array $a): bool => ! ($a['quick'] ?? false)));
$moreLabel ??= $rowLabel !== ''
? __('More actions for :row', ['row' => $rowLabel])
: __('More actions');
// `$instanceId` is the id to dispatch FROM. `anchored` defaults to true, so the
// overflow menu teleports to <body>: an Alpine `$dispatch` from a menu
// item bubbles to <body> and never reaches a host that wraps this
// component in the page, while the same event from a quick action (which
// is not teleported) does reach it. The contract would depend on which
// control the operator used. Both paths now dispatch from this
// component's own root, which stays where the caller put it. The caller's
// own id wins when given, so two rows never collide.
// The payload is encoded with Js::from, never string-interpolated.
$actionHandler = static function (array $action) use ($instanceId): string {
return 'document.getElementById('.\Illuminate\Support\Js::from($instanceId).')'
."?.dispatchEvent(new CustomEvent('row-actions:action', "
.'{ detail: '.\Illuminate\Support\Js::from($action['original'] ?? $action).', bubbles: true }))';
};
$namedAction = static function (array $action) use ($rowLabel): string {
$label = (string) ($action['label'] ?? '');
return $rowLabel !== '' ? __(':action for :row', ['action' => $label, 'row' => $rowLabel]) : $label;
};
$display = in_array($display, ['both', 'label', 'icon'], true) ? $display : 'both';
$hasIcon = static fn (array $action): bool => ! empty($action['icon']);
$showIcon = static fn (array $action): bool => $display !== 'label' && $hasIcon($action);
$iconOnly = static fn (array $action): bool => $display === 'icon' && $hasIcon($action);
// An overflow item opens its Alpine confirm after the menu has closed and
// handed focus back to its trigger, so the dialog returns focus there. It
// finds the dialog through this root's id, which a Livewire morph updates
// together with this expression (a generated id changes on every render).
$openLater = static fn (array $action): string => 'setTimeout(() => { const dialog = document.getElementById('
.\Illuminate\Support\Js::from($instanceId).')?.querySelector('
.\Illuminate\Support\Js::from('[data-row-confirm="'.$action['confirmation']['slug'].'"]')
.'); if (dialog && window.Alpine) window.Alpine.$data(dialog).show(); })';
$overflowConfirms = array_values(array_filter($overflow, static fn (array $a): bool => $a['confirmation'] !== null));
@endphp
@if ($quick !== [] || $overflow !== [])
<div
data-slot="row-actions"
id="{{ $instanceId }}"
{{ $attributes->except('id')->merge(['class' => 'flex min-w-0 max-w-full items-center justify-end gap-2 overflow-hidden']) }}
>
@foreach ($quick as $action)
@php
$kind = $action['kind'];
$confirmation = $action['confirmation'];
@endphp
@if ($confirmation !== null)
{{-- The trigger sits inside the alert dialog's root (display:
contents), so the Alpine mode returns focus to it. --}}
<x-ui.alert-dialog
:native="$confirmation['native']"
:name="$confirmation['name']"
:form-action="$confirmation['native'] ? ($action['url'] ?? '#') : null"
:form-method="$action['method']"
:form-hidden-fields="$action['fields']"
:wire-action="$confirmation['native'] ? null : $action['wire']"
:wire-params="$action['params']"
data-row-confirm="{{ $confirmation['slug'] }}"
>
@if ($confirmation['native'])
<x-ui.button type="button" variant="outline" size="sm" class="shrink-0" command="show-modal" :commandfor="$confirmation['dialogId']" aria-haspopup="dialog" data-row-action="{{ $action['key'] ?? '' }}" :aria-label="$iconOnly($action) ? null : $namedAction($action)" :icon-label="$iconOnly($action) ? $namedAction($action) : null" :title="$action['label']">
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
@unless ($iconOnly($action)){{ $action['label'] }}@endunless
</x-ui.button>
@else
<x-ui.button type="button" variant="outline" size="sm" class="shrink-0" x-ref="trigger" x-on:click="show()" x-bind:disabled="busy" aria-haspopup="dialog" data-row-action="{{ $action['key'] ?? '' }}" :aria-label="$iconOnly($action) ? null : $namedAction($action)" :icon-label="$iconOnly($action) ? $namedAction($action) : null" :title="$action['label']">
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
@unless ($iconOnly($action)){{ $action['label'] }}@endunless
</x-ui.button>
@endif
<x-ui.alert-dialog.content>
<x-ui.alert-dialog.header>
<x-ui.alert-dialog.title>{{ $confirmation['title'] }}</x-ui.alert-dialog.title>
<x-ui.alert-dialog.description>{{ $confirmation['description'] }}</x-ui.alert-dialog.description>
</x-ui.alert-dialog.header>
<x-ui.alert-dialog.footer>
<x-ui.alert-dialog.cancel>{{ $confirmation['cancel'] }}</x-ui.alert-dialog.cancel>
<x-ui.alert-dialog.action :variant="$action['destructive'] ? 'destructive' : 'default'" :busy-label="$confirmation['busyLabel']">{{ $confirmation['label'] }}</x-ui.alert-dialog.action>
</x-ui.alert-dialog.footer>
</x-ui.alert-dialog.content>
</x-ui.alert-dialog>
@elseif ($kind === 'submit')
@if ($action['method'] === 'POST' && $action['fields'] === [])
<form method="POST" action="{{ $action['url'] ?? '#' }}" class="min-w-0 shrink-0">
@csrf
<x-ui.button type="submit" variant="outline" size="sm" :aria-label="$iconOnly($action) ? null : $namedAction($action)" :icon-label="$iconOnly($action) ? $namedAction($action) : null" :title="$action['label']">
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
@unless ($iconOnly($action)){{ $action['label'] }}@endunless
</x-ui.button>
</form>
@else
{{-- PUT, PATCH, DELETE or hidden fields: the form item adds
CSRF, the spoofed _method field and the hidden inputs. --}}
<x-ui.form :action="$action['url'] ?? '#'" :method="$action['method']" :hidden-fields="$action['fields']" class="min-w-0 shrink-0">
<x-ui.button type="submit" variant="outline" size="sm" :aria-label="$iconOnly($action) ? null : $namedAction($action)" :icon-label="$iconOnly($action) ? $namedAction($action) : null" :title="$action['label']">
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
@unless ($iconOnly($action)){{ $action['label'] }}@endunless
</x-ui.button>
</x-ui.form>
@endif
@elseif ($kind === 'wire')
<x-ui.button
type="button"
variant="outline"
size="sm"
class="shrink-0"
wire:click="{{ $action['wireCall'] }}"
:wire-target="$action['wire']"
:aria-label="$iconOnly($action) ? null : $namedAction($action)"
:icon-label="$iconOnly($action) ? $namedAction($action) : null"
:title="$action['label']"
>
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
@unless ($iconOnly($action)){{ $action['label'] }}@endunless
</x-ui.button>
@elseif ($kind === 'dialog')
<x-ui.button
type="button"
variant="outline"
size="sm"
class="shrink-0"
:aria-label="$iconOnly($action) ? null : $namedAction($action)"
:icon-label="$iconOnly($action) ? $namedAction($action) : null"
:title="$action['label']"
x-on:click="{{ $actionHandler($action) }}"
>
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
@unless ($iconOnly($action)){{ $action['label'] }}@endunless
</x-ui.button>
@else
<x-ui.button
as="a"
href="{{ $action['url'] ?? '#' }}"
variant="outline"
size="sm"
class="shrink-0"
:aria-label="$iconOnly($action) ? null : $namedAction($action)"
:icon-label="$iconOnly($action) ? $namedAction($action) : null"
:title="$action['label']"
:target="($action['newTab'] ?? false) ? '_blank' : null"
:rel="($action['newTab'] ?? false) ? 'noopener noreferrer' : null"
>
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
@unless ($iconOnly($action)){{ $action['label'] }}@endunless
</x-ui.button>
@endif
@endforeach
@if ($overflow !== [])
<x-ui.dropdown :anchored="$anchored" class="shrink-0">
<x-ui.dropdown.trigger
:aria-label="$moreLabel"
:title="$moreLabel"
class="inline-flex size-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring"
>
<svg aria-hidden="true" viewBox="0 0 16 16" fill="currentColor" class="size-4">
<circle cx="3" cy="8" r="1.25" />
<circle cx="8" cy="8" r="1.25" />
<circle cx="13" cy="8" r="1.25" />
</svg>
</x-ui.dropdown.trigger>
<x-ui.dropdown.content align="end">
@foreach ($overflow as $action)
@php
$kind = $action['kind'];
$itemVariant = $action['destructive'] ? 'destructive' : 'default';
@endphp
@if ($kind === 'link')
<x-ui.dropdown.item
:href="$action['url'] ?? '#'"
:target="($action['newTab'] ?? false) ? '_blank' : null"
:rel="($action['newTab'] ?? false) ? 'noopener noreferrer' : null"
:variant="$itemVariant"
>
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
{{ $action['label'] }}
</x-ui.dropdown.item>
@elseif ($action['confirmation'] !== null && $action['confirmation']['native'])
{{-- Opens the native confirm below with an invoker command. --}}
<x-ui.dropdown.item command="show-modal" :commandfor="$action['confirmation']['dialogId']" aria-haspopup="dialog" data-row-action="{{ $action['key'] ?? '' }}" :variant="$itemVariant">
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
{{ $action['label'] }}
</x-ui.dropdown.item>
@elseif ($action['confirmation'] !== null)
<x-ui.dropdown.item :x-on:click="$openLater($action)" aria-haspopup="dialog" data-row-action="{{ $action['key'] ?? '' }}" :variant="$itemVariant">
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
{{ $action['label'] }}
</x-ui.dropdown.item>
@elseif ($kind === 'wire')
<x-ui.dropdown.item wire:click="{{ $action['wireCall'] }}" :variant="$itemVariant">
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
{{ $action['label'] }}
</x-ui.dropdown.item>
@else
{{-- submit/dialog: dropdown-item's button is fixed
type="button", so both non-link kinds dispatch
the same event for the host page to act on
(submit its own form, open a confirm dialog, …). --}}
<x-ui.dropdown.item :x-on:click="$actionHandler($action)" :variant="$itemVariant">
@if ($showIcon($action))
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0">{!! $action['icon'] !!}</svg>
@endif
{{ $action['label'] }}
</x-ui.dropdown.item>
@endif
@endforeach
</x-ui.dropdown.content>
</x-ui.dropdown>
@endif
{{-- The confirm steps of overflow actions sit outside the teleported
menu, so they stay in the page (and in the Livewire component)
after the menu closes. --}}
@foreach ($overflowConfirms as $action)
@php $confirmation = $action['confirmation']; @endphp
<x-ui.alert-dialog
:native="$confirmation['native']"
:name="$confirmation['name']"
:form-action="$confirmation['native'] ? ($action['url'] ?? '#') : null"
:form-method="$action['method']"
:form-hidden-fields="$action['fields']"
:wire-action="$confirmation['native'] ? null : $action['wire']"
:wire-params="$action['params']"
data-row-confirm="{{ $confirmation['slug'] }}"
>
<x-ui.alert-dialog.content>
<x-ui.alert-dialog.header>
<x-ui.alert-dialog.title>{{ $confirmation['title'] }}</x-ui.alert-dialog.title>
<x-ui.alert-dialog.description>{{ $confirmation['description'] }}</x-ui.alert-dialog.description>
</x-ui.alert-dialog.header>
<x-ui.alert-dialog.footer>
<x-ui.alert-dialog.cancel>{{ $confirmation['cancel'] }}</x-ui.alert-dialog.cancel>
<x-ui.alert-dialog.action :variant="$action['destructive'] ? 'destructive' : 'default'" :busy-label="$confirmation['busyLabel']">{{ $confirmation['label'] }}</x-ui.alert-dialog.action>
</x-ui.alert-dialog.footer>
</x-ui.alert-dialog.content>
</x-ui.alert-dialog>
@endforeach
</div>
@endif
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.4.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