Skip to content
Brok UI

Loading…

No results

Row Actions

Open source

The eligible quick action(s) for a table row inline, plus an overflow menu for the rest, driven by an actions array.

Version
v1.4.0
Stability
stable
License
MIT
Related
Button
Dropdown Menu
Admin Table
Action Bar

Preview

previews.components.row-actions.default.blade.php 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'],
    ]"
/>

Installation

terminal
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:

resources/js/ui/index.js JS
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.

  • blade resources/views/components/ui/row-actions.blade.php
Registry dependencies
alert-dialog button dropdown form
Packages
composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

row-actions.md
# 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

confirm.blade.php Blade
{{-- 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.blade.php Blade
{{-- `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>
icons.blade.php Blade
{{-- 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.blade.php Blade
<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

manifest knowledge + registry-derived coverage

Props

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

row-actions

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

Scannable data display

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
row-actions
Theming hooks
row-actions

Accessibility

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

resources/views/components/ui/row-actions.blade.php Blade
{{--
    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