Skip to content

Kinetix Actions & Confirmation Modals

Kinetix Actions are a fluent PHP builder for interactive buttons and links. The same Action class powers notification buttons, table record actions, and table toolbar actions. This guide focuses on building actions and gating destructive ones behind a confirmation modal.

For notification-specific action behaviour (markAsRead(), close()), see the main README.


1. Building an Action

php
use Happones\Kinetix\Actions\Action;

Action::make('edit')
    ->label('Edit')
    ->icon('edit')
    ->color('primary')
    ->url(fn ($record) => route('users.edit', $record));

Core API

MethodDescription
::make(string $name)Create an action
->label(string)Button text
->icon(string, string $position = 'before')Lucide icon name
->url(string|Closure, bool $newTab = false)Navigate on click; closure receives the record
->inertiaVisit(string $url, array $options = [])SPA visit via router.visit() (supports method)
->dispatch(string $event, array $data = [])Fire a kinetix:{event} browser event
->button() / ->link()Render style
->color(string)primary · secondary · success · warning · info · danger · gray
->icon(?string, $position = 'before')Lucide icon name; pass null to remove it
->size(string)xs · sm · md · lg

Colors map to shadcn tokens (themeable, dark-mode aware), so you can reproduce the "classic" admin palette per action when you want it:

ColorTokenLooks like
primaryprimarybrand/solid
infoinfoblue (e.g. View/Show)
warningwarningamber/yellow (e.g. Edit)
successsuccessgreen (e.g. Create)
dangerdestructivered (e.g. Delete)
gray / secondaryoutline / secondaryneutral
php
ViewAction::make()->color('info'),      // blue
EditAction::make()->color('warning'),   // amber
CreateAction::make()->color('success'), // green
// DeleteAction is danger (red) by default

shadcn guidance: by default Kinetix keeps secondary actions neutral (outline/ghost) and only delete red — distinguishing actions by icon, which is the idiomatic shadcn approach. Reach for the colored palette above only if you specifically want the classic colored-button look; it stays token-based either way.


2. Confirmation Modals

Add requiresConfirmation() to any action. The action only runs after the user confirms in a modal — ideal for destructive operations like deletes.

php
Action::make('delete')
    ->label('Delete')
    ->icon('trash')
    ->color('danger')
    ->requiresConfirmation()
    ->modalHeading('Delete user?')
    ->modalDescription('This permanently removes the account and cannot be undone.')
    ->modalSubmitActionLabel('Delete')
    ->modalCancelActionLabel('Keep')
    ->inertiaVisit(fn ($record) => route('users.destroy', $record), ['method' => 'delete']);

Shorthand

Pass the heading straight to requiresConfirmation():

php
Action::make('archive')
    ->requiresConfirmation('Archive this record?')
    ->color('warning')
    ->inertiaVisit(fn ($r) => route('records.archive', $r), ['method' => 'post']);

Confirmation API

MethodDescriptionDefault
->requiresConfirmation(bool|string $condition = true)Enable the modal; a string also sets the headingfalse
->modalHeading(string)Modal titlet('kinetix.confirm_heading') → "Are you sure?"
->modalDescription(string)Body text
->modalIcon(string)Lucide icon shown in the modalalert-triangle
->modalSubmitActionLabel(string)Confirm button labelt('kinetix.confirm') → "Confirm"
->modalCancelActionLabel(string)Cancel button labelt('kinetix.cancel') → "Cancel"

The confirm button inherits the action's ->color(), so a danger action gets a red confirm button automatically.


3. Behaviour & Lifecycle

When requiresConfirmation() is set, clicking the action button opens KinetixConfirmModal.vue instead of running immediately. The action runs only on confirm.

->inertiaVisit() vs ->request()

MethodUse whenBehaviour
->inertiaVisit($url, ['method' => 'post'])The route returns an Inertia response (redirect()/back()/Inertia::render)router.visit() — full Inertia visit (updates page props).
->request($url, ['method' => 'post', 'toast' => '…'])The route returns JSON and you just want a background call + a toast (no navigation)Plain fetch() XHR (with the XSRF token); shows the toast on success. No Inertia involvement.

Avoiding Inertia's "invalid response" modal: an ->inertiaVisit() to an endpoint that returns JSON (instead of an Inertia redirect/render) makes Inertia pop its error modal. For fire-and-forget endpoints (queue a job, then notify), use ->request() so no Inertia visit happens. This is exactly what ExportAction uses — click → background POST → "Export queued" toast → a download notification arrives when the job finishes.


4. Using Actions in Tables

Register actions on a Table and they are serialized with each record (record actions) or once for the toolbar:

php
use Happones\Kinetix\Tables\Table;
use Happones\Kinetix\Actions\Action;

Table::make(User::query())
    ->recordActions([
        Action::make('edit')->icon('edit')->url(fn ($u) => route('users.edit', $u)),
        Action::make('delete')
            ->icon('trash')->color('danger')
            ->requiresConfirmation('Delete this user?')
            ->inertiaVisit(fn ($u) => route('users.destroy', $u), ['method' => 'delete']),
    ])
    ->toolbarActions([
        Action::make('create')->label('New user')->icon('plus')->url(route('users.create')),
    ]);

KinetixTable.vue handles the full execution flow (dispatch / Inertia visit / new tab / navigation) and the confirmation gate — no extra frontend wiring is needed.


5. The Confirmation Modal Component

KinetixConfirmModal.vue is a self-contained, reusable dialog you can drive directly:

vue
<script setup lang="ts">
import { ref } from 'vue';
import KinetixConfirmModal from '@/components/kinetix/KinetixConfirmModal.vue';

const open = ref(false);
const onConfirm = () => { /* ... */ };
</script>

<template>
    <button @click="open = true">Delete</button>

    <KinetixConfirmModal
        v-model:open="open"
        color="danger"
        heading="Delete item?"
        description="This cannot be undone."
        @confirm="onConfirm"
    />
</template>
PropTypeDescription
openbooleanVisibility (use v-model:open)
headingstring?Title (falls back to the i18n default)
descriptionstring?Body text
iconstring?Lucide icon name
colorstring?Themes the confirm button + icon (danger default)
submitLabel / cancelLabelstring?Button labels (fall back to i18n)

Events: confirm, cancel, update:open.

The modal is rendered through <Teleport to="body">, closes on overlay click or Escape, and removes its keydown listener when closed or unmounted — so it leaves no lingering global handlers.


6. Page Action Bars

KinetixPageHeader.vue renders a page-level header with a title, optional description, and a right-aligned row of actions — the standard place for "Create", "Edit", "Delete", or custom page actions. It reuses the same action execution and confirmation flow as tables.

Backend

Build the actions in PHP and pass them as an array:

php
use Happones\Kinetix\Actions\Action;

return inertia('Users/Edit', [
    'user' => $user,
    'headerActions' => [
        Action::make('view')
            ->label('View')->icon('eye')->color('gray')
            ->url(route('users.show', $user)),

        Action::make('delete')
            ->label('Delete')->icon('trash')->color('danger')
            ->requiresConfirmation('Delete this user?')
            ->inertiaVisit(route('users.destroy', $user), ['method' => 'delete']),
    ],
]);

Frontend

vue
<script setup lang="ts">
import KinetixPageHeader from '@/components/kinetix/KinetixPageHeader.vue';
import type { KinetixAction } from '@/types';

defineProps<{ headerActions: KinetixAction[] }>();
</script>

<template>
    <KinetixPageHeader
        heading="Edit user"
        description="Update the account details below."
        :actions="headerActions"
    >
        <!-- Optional extra controls via the default slot -->
    </KinetixPageHeader>
</template>
PropTypeDescription
headingstring?Page title
descriptionstring?Sub-heading text
actionsKinetixAction[]Serialized actions rendered as buttons/links

Slots: before-actions (left of the action row) and the default slot (right of it). Actions with requiresConfirmation() open the shared confirmation modal automatically.

Shared execution composable

Both KinetixTable.vue and KinetixPageHeader.vue consume @/composables/useKinetixActions:

  • executeAction(action) — runs an action (dispatch / Inertia visit / new tab / navigation).
  • useActionConfirmation() — returns { pendingAction, isConfirmOpen, requestAction, confirm, cancel } to gate actions behind the modal.

Wire any new action-rendering component through this composable so behaviour stays consistent.


7. Action Groups (Dropdowns)

ActionGroup collapses several actions into a single dropdown trigger — useful for keeping record rows and toolbars compact.

Where ActionGroups work: recordActions, toolbarActions/headerActions, and footerActions render groups as dropdowns. bulkActions do not — the bulk bar renders flat buttons and a dropdown wouldn't forward the selected ids. So you can put an Export (or any) action inside a group in the toolbar/header/footer (it acts on the whole/filtered table), but for export-selected use a flat bulkActions entry (see Tables → Bulk Actions). The same Export Action can be both: inside a toolbar group and a flat bulk action.

php
use Happones\Kinetix\Actions\Action;
use Happones\Kinetix\Actions\ActionGroup;

ActionGroup::make([
    Action::make('edit')->label('Edit')->icon('edit')->url(fn ($r) => route('users.edit', $r)),
    Action::make('view')->label('View')->icon('eye')->url(fn ($r) => route('users.show', $r)),
    Action::make('delete')->label('Delete')->icon('trash')->color('danger')
        ->requiresConfirmation('Delete this user?')
        ->inertiaVisit(fn ($r) => route('users.destroy', $r), ['method' => 'delete']),
])
    ->label('Actions')      // optional — omit for an icon-only trigger
    ->icon('ellipsis-vertical');
MethodDescriptionDefault
::make(array $actions)Actions shown in the menu
->actions(array)Replace the action list
->label(string)Trigger label (omit for icon-only)
->icon(string)Trigger iconellipsis-vertical
->color(string) / ->size(string)Trigger stylinggray / sm

Groups serialize to an ActionData with type: 'group' and a nested actions array, so they can be dropped straight into a table's recordActions() / toolbarActions() or a page header's actions alongside regular actions:

php
Table::make(User::query())->recordActions([
    Action::make('edit')->icon('edit')->url(fn ($u) => route('users.edit', $u)),
    ActionGroup::make([
        Action::make('archive')->icon('archive')->requiresConfirmation('Archive?')
            ->inertiaVisit(fn ($u) => route('users.archive', $u), ['method' => 'post']),
        Action::make('delete')->icon('trash')->color('danger')->requiresConfirmation('Delete?')
            ->inertiaVisit(fn ($u) => route('users.destroy', $u), ['method' => 'delete']),
    ]),
]);

KinetixActionDropdown.vue renders the menu. It closes on outside click or Escape and removes those listeners on close/unmount (leak-safe), and routes each item through the shared confirmation flow.


8. Prebuilt CRUD actions

Convenience subclasses with sensible defaults (label, icon, color) and a default policy ability. Each is a normal Action, so every method above still applies.

php
use Happones\Kinetix\Actions\CreateAction;
use Happones\Kinetix\Actions\EditAction;
use Happones\Kinetix\Actions\ViewAction;
use Happones\Kinetix\Actions\DeleteAction;

Table::make(Post::query())
    ->recordActions([
        ViewAction::make()->url(fn ($post) => route('posts.show', $post)),
        EditAction::make()->url(fn ($post) => route('posts.edit', $post)),
        DeleteAction::make()->inertiaVisit(fn ($post) => route('posts.destroy', $post), ['method' => 'delete']),
    ])
    ->toolbarActions([
        CreateAction::make()->url(route('posts.create'))->authorize('create', Post::class),
    ]);
ActionDefaultsDefault policy ability
ViewActionlabel View, icon eyeview (per record)
EditActionlabel Edit, icon editupdate (per record)
DeleteActionlabel Delete, icon trash, color danger, requiresConfirmation()delete (per record)
CreateActionlabel Create, icon plus, color primarynone — pass ->authorize('create', Model::class)
RestoreActionlabel Restore, icon rotate-ccw; only visible on trashed rowsrestore (per record)
ForceDeleteActionlabel Delete permanently, icon trash-2, danger, requiresConfirmation(); only on trashed rowsforceDelete (per record)

RestoreAction / ForceDeleteAction are for SoftDeletes models and auto-hide on non-trashed records (via a visible() check on $record->trashed()). Pair them with a TrashedFilter (Tables → Filters).

Labels come from the kinetix i18n namespace and respect the active locale.

File actions: DownloadAction & PreviewAction

php
use Happones\Kinetix\Actions\DownloadAction;
use Happones\Kinetix\Actions\PreviewAction;

$table->recordActions([
    PreviewAction::make()->url(fn ($doc) => route('docs.show', $doc)),         // image/pdf detected from the URL
    PreviewAction::make()->preview('pdf')->url(fn ($doc) => $doc->pdf_url),     // force a type
    DownloadAction::make()->url(fn ($doc) => route('docs.download', $doc)),     // direct download
]);
ActionDefaultsBehaviour
PreviewActionlabel Preview, icon eye, color grayOpens url in the file-preview lightbox (zoomable image / embedded PDF, with a download button). ->preview('image'|'pdf'|'auto') sets the type.
DownloadActionlabel Download, icon download, color grayForces a browser download of url (synthetic <a download> click).

Both are plain Actions, so ->color(), ->icon(), ->label(), ->authorize(), ->visible() all apply. The underlying flags are Action::download() and Action::preview($type).

Mount the lightbox once. For PreviewAction (and ImageColumn::preview()) to render, add <KinetixFilePreview /> once in your app layout — it listens for the kinetix:preview window event, like the notification components.


9. Authorization & visibility

Actions are authorized on the server. An action that fails its check is omitted from the serialized payload entirely — the frontend never receives it (so it can't be revealed by tampering with the client). This is the recommended approach over sending every action plus a "can" flag to Vue.

php
// Laravel policy ability — checked against the row record via Gate::allows($ability, $record):
EditAction::make()->authorize('update');

// Explicit subject (e.g. a create action with no record):
CreateAction::make()->authorize('create', Post::class);

// Any custom logic:
Action::make('publish')->authorize(fn ($record) => auth()->user()->isEditor());

// Manual visibility (also evaluated server-side):
Action::make('archive')->visible(fn ($record) => ! $record->archived);
Action::make('legacy')->hidden();
MethodBehaviour
->authorize(string $ability, mixed $subject = null)Gate::allows($ability, $subject ?? $record) (Laravel policies)
->authorize(Closure $cb)$cb($record) returns a boolean
->authorize(bool)Static gate
->visible(bool|Closure) / ->hidden(bool|Closure)Manual show/hide

Table automatically drops unauthorized record/toolbar actions (and per row). ActionGroup drops unauthorized children, and supports ->authorize()/->visible() on the group itself. For page headers or other manual contexts, serialize a set with Action::toArrayMany([...], $record) — it returns only the actions the current user may perform:

php
return inertia('Posts/Edit', [
    'headerActions' => \Happones\Kinetix\Actions\Action::toArrayMany([
        EditAction::make()->url(route('posts.edit', $post)),
        DeleteAction::make()->inertiaVisit(route('posts.destroy', $post), ['method' => 'delete']),
    ], $post),
]);

10. Localization

Default modal labels come from the kinetix translation namespace (confirm, cancel, confirm_heading), shipped in English, Spanish, French, and Portuguese.

Released under the MIT License.