EN
Getting started
77 · DataTable

DataTable engine

A ready-made Livewire table whose columns, filters and actions are defined in a PHP class. Search, filters, sorting, selection and pagination run on the server; only the visible page is sent to the browser. Detailed guide: DataTable.

PRO AcunDataTable
01

Table class

Extend the DataTable class (or its alias AcunDataTable); query() and columns() are required. Record visibility is limited by the scope in query(), access to the table by authorizeTable(). Use it on a page with <livewire:orders-table />.

PHP
use Acun\Ui\DataTable\DataTable;

final class OrdersTable extends DataTable
{
    public string $title = 'Orders';

    protected function query(): Builder
    {
        return Order::query()->visibleTo(auth()->user())->select(['id', 'number', 'customer_name', 'is_paid']);
    }

    protected function columns(): array
    {
        return [
            Column::make('Order no.', 'number')->searchable()->sortable(),
            Column::make('Customer', 'customer_name')->searchable(SearchMode::Contains)->sortable(),
            Column::make('Paid', 'is_paid')->boolean(),
        ];
    }

    protected function authorizeTable(): void
    {
        $this->authorize('viewAny', Order::class);
    }
}
PropValues
query()Required: the base query
columns()Required: list of Column
$titleTitle in the toolbar
02

Columns

Defined with Column::make(label, field); for a relation, the field can use dot notation. Formatters write the value in the format of the app language. The output of renderUsing() is escaped; for cells that contain HTML, use a view() that receives $row and $column. priority() sets when the column is hidden on narrow screens: 1 and 2 are always visible, 3 is hidden below 38rem (608px), 4 and up below 52rem (832px).

PHP
Column::make('Amount', 'total')->money('TRY')->align('right')->width('140px');
Column::make('Quantity', 'quantity')->number()->align('center');
Column::make('Delivery date', 'delivered_on')->date()->sortable()->priority(3);
Column::make('Created', 'created_at')->dateTime()->hidden();
Column::make('Customer', 'customer.name')->nowrap();
Column::make('Agent', 'agent_id')->renderUsing(fn ($row, $value) => $row->agent?->full_name ?? '—');
Column::make('Status', 'is_active')->view('tables.cells.status');
PropValues
sortable() · searchable(mode)Adds the column to the sort and search lists
hidden() · width() · align() · nowrap()Appearance; align: left | center | right
wrap()Long text wraps to the next line, also in a scrollX() table; use it with width()
priority(n)1 | 2 (always visible) · 3 (hidden < 38rem) · 4 and up (hidden < 52rem)
date() · dateTime() · number() · money() · boolean()Date, date and time, number and currency in the app language's format · Active/Inactive
renderUsing(fn) · view(name)Custom cell content
04

Filters

By default, filters are listed in the panel opened with the Filters button in the toolbar. select and boolean come with an "All" option, and a boolean without options shows Yes/No. Range filters show two fields; in a number range they carry the "Min" and "Max" hints. Date filters compare by day. For a project-specific condition, use applyUsing().

PHP
protected function filters(): array
{
    return [
        Filter::select('Status', 'is_active')->options(['1' => 'Active', '0' => 'Inactive']),
        Filter::multiSelect('Region', 'region')->options(['east' => 'East', 'west' => 'West']),
        Filter::dateRange('Delivery date', 'delivered_on'),
        Filter::numberRange('Amount', 'total'),
        Filter::boolean('Gift wrapped', 'is_gift'),
        Filter::text('Customer', 'customer')
            ->applyUsing(fn (Builder $query, string $value) => $query->whereHas('customer', fn ($q) => $q->where('name', 'like', "%{$value}%"))),
    ];
}
PropValues
Filter::text | select | multiSelect | boolean | date | dateRange | number | numberRange
options([...])Options for select, multiSelect and boolean
applyUsing(fn ($query, $value))Replaces the default condition
05

Toolbar filters

Move frequently used filters out of the panel into the toolbar with inToolbar(): they stay visible on their own row below the title, the search and the buttons. Each one is a single bordered group with its label on the left (e.g. Status | All). A multi-select opens a menu with checkboxes; its summary reads "All" or "N selected". The panel lists only the filters that are not in the toolbar; the badge on the Filters button counts only those, and "Clear filters" in the panel clears only those. When all filters are in the toolbar, the Filters button is not shown.

PHP
protected function filters(): array
{
    return [
        Filter::select('Status', 'is_active')->options(['1' => 'Active', '0' => 'Inactive'])->inToolbar(),
        Filter::multiSelect('Region', 'region')->options(['east' => 'East', 'west' => 'West', 'south' => 'South'])->inToolbar(),
        Filter::dateRange('Delivery date', 'delivered_on'),   // stays in the panel
    ];
}
PropValues
inToolbar()Shows the filter in the toolbar
inDrawer()Keeps it in the Filters panel (the default; for writing it explicitly)
toggleFilterValue($key, $value)The method the multi-select menu calls; it accepts only a defined multi-select filter and one of its defined options, then resets to the first page and clears the selection
activeFilterCount(true | false)Counts only the toolbar (true) or panel (false) filters; all of them without an argument
clearFilters(true | false)Clears only the toolbar or panel filters; all of them without an argument
06

Row actions

Listed in the "•••" menu at the end of each row. visible() hides the action on that row, authorize() disables the button; both are checked again on the server when the action runs. The action receives ($row, $table); style('danger') shows it in red. icon() takes an acun:icon name and puts a small (1rem) icon before the label.

PHP
protected function actions(): array
{
    return [
        Action::make('Edit')
            ->icon('pencil-square')
            ->authorize(fn (Order $order) => auth()->user()->can('update', $order))
            ->action(fn (Order $order) => $this->redirectRoute('orders.edit', $order)),

        Action::make('Delete')
            ->icon('trash')
            ->style('danger')
            ->visible(fn (Order $order) => $order->isCancelled())
            ->authorize(fn (Order $order) => auth()->user()->can('delete', $order))
            ->action(fn (Order $order) => $order->delete()),
    ];
}
PropValues
icon(name)Name of the acun:icon before the label (e.g. pencil-square)
visible(fn)Whether to show it on the row
authorize(fn)Permission to run it
style()danger: red
action(fn ($row, $table))The action
07

Bulk actions and row authorization

When a selection is made, a selection bar appears above the table; "Select all N records matching the filters" selects all results. The action receives a query containing the selected records and runs after a confirmation prompt. authorize() checks the action as a whole and authorizeRow() checks each targeted record on the server: if even one record fails, no record is touched and a 403 is returned. As with row actions, icon() adds an icon before the label on the button.

PHP
protected function bulkActions(): array
{
    return [
        BulkAction::make('Mark as shipped')
            ->icon('truck')
            ->authorize(fn () => auth()->user()->can('updateAny', Order::class))
            ->action(fn (Builder $query) => $query->update(['status' => OrderStatus::Shipped])),

        BulkAction::make('Delete')
            ->icon('trash')
            ->style('danger')
            ->authorize(fn () => auth()->user()->can('deleteAny', Order::class))
            ->authorizeRow(fn (Order $order) => auth()->user()->can('delete', $order))
            ->action(fn (Builder $query) => $query->delete()),
    ];
}
PropValues
icon(name)Name of the acun:icon before the label on the button (e.g. truck)
style()danger: red button
authorize(fn)Permission for the action as a whole
authorizeRow(fn ($row))Permission for each record; if one fails, the action stops
action(fn ($query, $table))Query of the selected records
08

Actions: toolbar, export and utilities

Override the headerActions(), exportActions() and utilityActions() methods in the table class; all of them are optional. On desktop, the page size and search sit on the left; Filters, Refresh, Columns, Export, the "⋮" menu and, last, the header actions sit on the right; on a phone, the first header action and an "Actions" button remain, and the button opens the other tools in a list. A hidden (visible) or unauthorized (can, authorize) action is not rendered, and if it is called from the browser, the server returns 403. action() takes a closure or the name of a (public or protected) method of the table. The browser side lives in the acunDataTable component in the PRO JS (import '@acunsoft/acun-ui-pro'). Details: the DataTable: all options guide, Actions section.

PHP
use Acun\Ui\DataTable\Actions\ExportAction;
use Acun\Ui\DataTable\Actions\TableAction;
use Acun\Ui\DataTable\Actions\UtilityAction;

protected function headerActions(): array
{
    return [
        TableAction::make('create')->label('New order')->icon('plus')
            ->url(fn () => route('orders.create'))
            ->can('create', Order::class)
            ->shortcut('mod+shift+n'),

        TableAction::make('sync')->label('Sync')->icon('arrow-path')->color('gray')
            ->action('syncOrders')                   // protected function syncOrders(): void
            ->confirm('Sync the orders?')
            ->loadingLabel('Syncing…')
            ->successMessage('Orders updated.'),
    ];
}

protected function exportActions(): array
{
    return [
        ExportAction::csv()->shortcut('mod+e'),
        ExportAction::pdf(),        // the print page → "Save as PDF" in the browser
        ExportAction::json()->can('export-json'),
        ExportAction::print(),
        ExportAction::copy(),       // current page or selection, as tab-separated text
    ];
}

protected function utilityActions(): array
{
    return [
        UtilityAction::refresh(),
        UtilityAction::columns(),
        UtilityAction::fullscreen(),
        UtilityAction::resetFilters(),
        UtilityAction::resetSorting(),
    ];
}

protected function defaultSort(): ?array
{
    return ['field' => 'created_at', 'direction' => 'desc'];
}
PropValues
label() · icon() · color() · variant() · tooltip()Appearance; icon is an acun:icon name
visible() · hidden() · disabled()A bool or a closure; a hidden action is not rendered and does not run
can($ability, $arguments) · authorize(fn)Gate::allows or a custom check; on the server, on every call
url($url, newTab: true) · action(fn | 'method')A link or an action
confirm($message, $title, $button)Confirmation with acun:confirmation-dialog
shortcut('mod+e') · loadingLabel() · successMessage()Shortcut (mod: Ctrl / ⌘), loading text, success notification
defaultSort()The order used until a column is chosen; "Reset sorting" returns to it
density()Row density: compact, normal or comfortable (not in the user menu)
rowDetails()(+) at the start of the row: all the fields of the row in a window; responsive · always · null
09

Export scope and queue

The "Export" menu first asks for the scope: Current page, Filtered records, Selected records (n) or All records (asks for confirmation). Default: the selection if there is one, the filtered records if there is a search or filter, otherwise the current page. Every scope keeps the query() scope and the sort; rows are read in chunks through the table's own buildQuery() pipeline. The columns are the visible columns, written as plain text in the same format as the cells. An export above queue_threshold is prepared by a queued job on behalf of the same user; when it finishes, the notification shows a signed download link that only its owner can use (php artisan queue:work is required). PDF opens the print page and the browser saves the file; no extra package is needed. CSV is written with a UTF-8 BOM and ;.

PHP
Column::make('Amount', 'amount')->money('TRY')->exportUsing(fn (Order $order) => $order->amount);
Column::make('Map', 'location')->view('cells.map')->exportable(false);
Column::make('Code', 'custom_code')->toggleable(false);     // cannot be hidden from the Columns menu

// Your own format: Contracts\ExportDriver (extension, mimeType, write)
ExportAction::make('xml')->label('XML')->icon('code-bracket')->driver(XmlExportDriver::class);

// Eager load relations into the export query
protected function exportQuery(ExportScope $scope): Builder
{
    return parent::exportQuery($scope)->with('customer');
}
PropValues
queue_thresholdUp to 5000 rows downloads right away; above that, it is queued
exportUsing(fn ($row, $value))The exported value of the column
exportable(false) · toggleable(false)Not exported · always visible
ExportScopecurrent_page · filtered · selected · all
10

Bulk and row actions (new classes)

Actions\BulkAction and Actions\RowAction are created by name (make('approve')). Presets: BulkAction::delete(), exportSelected('csv'), printSelected(); RowAction::view($url), edit($url), delete(). A bulk action closure can receive $query, $ids, $records and $count, and asks for confirmation by default. When more than three actions are visible on a row, the first two are buttons and the rest are in the "⋮" menu. The old Action and BulkAction keep working as before.

PHP
use Acun\Ui\DataTable\Actions\BulkAction;
use Acun\Ui\DataTable\Actions\RowAction;

protected function bulkActions(): array
{
    return [
        BulkAction::make('ship')->label('Mark as shipped')->icon('truck')
            ->action(fn (Builder $query) => $query->update(['status' => OrderStatus::Shipped]))
            ->successMessage(':count orders marked as shipped.'),
        BulkAction::exportSelected('csv'),
        BulkAction::printSelected(),
        BulkAction::delete()->authorizeRow(fn (Order $order) => auth()->user()->can('delete', $order)),
    ];
}

protected function actions(): array
{
    return [
        RowAction::view(fn (Order $order) => route('orders.show', $order)),
        RowAction::edit(fn (Order $order) => route('orders.edit', $order))->can('update'),
        RowAction::make('archive')->label('Archive')->icon('archive-box')
            ->visible(fn (Order $order) => ! $order->isArchived())
            ->action(fn (Order $order) => $order->archive()),
        RowAction::delete()->can('delete'),
    ];
}
11

Empty state

The default texts are "No results found." when there is a search or filter and "No records yet." when there is not, both in the app language. Override the emptyState() method for your own texts.

PHP
protected function emptyState(): array
{
    return $this->search !== '' || $this->activeFilterCount() > 0
        ? ['title' => 'No matching orders.', 'description' => 'Change the search or the filters.']
        : ['title' => 'No orders yet.', 'description' => 'Create the first order with the New order button.'];
}
12

Example: Basic

The smallest DataTable: query() says which records to list and columns() which columns. Search, sorting and pagination come automatically. Demo: https://demo.acunsoft.test/admin/tables/datatable/basic

PHP
final class BasicOrdersTable extends DataTable
{
    protected function query(): Builder
    {
        return Order::query();
    }

    protected function columns(): array
    {
        return [
            Column::make('Order no.', 'number')->searchable()->sortable(),
            Column::make('Customer', 'customer_name')->searchable('contains')->sortable(),
            Column::make('Amount', 'amount')->money('TRY')->align('right')->sortable(),
            Column::make('Order date', 'ordered_at')->dateTime()->sortable(),
        ];
    }
}
13

Example: Responsive

priority(3) columns are hidden below 38rem and priority(4) columns below 52rem; rowDetails('responsive') puts a (+) at the start of the row only while a column is hidden and shows the hidden fields in a window. Narrow the window or open it on a phone. Demo: https://demo.acunsoft.test/admin/tables/datatable/responsive

PHP
protected function columns(): array
{
    return [
        Column::make('Order no.', 'number')->sortable(),
        Column::make('Customer', 'customer_name'),
        Column::make('Status', 'status')->view('tables.cells.order-status')->priority(3),
        Column::make('Product', 'product')->priority(4),
        Column::make('City', 'city')->priority(4),
    ];
}

protected function rowDetails(): ?string
{
    return 'responsive';
}
14

Example: Scrolling table

scrollX() keeps cells on a single line and scrolls a wide table sideways (Column::wrap() columns wrap to the next line); scrollY() gives the box a height, so the table scrolls down inside the box and the header row stays at the top. It works with CSS; no JS is needed. Demo: https://demo.acunsoft.test/admin/tables/datatable/scroll

PHP
protected function scrollX(): bool
{
    return true;
}

protected function scrollY(): ?string
{
    return '400px';   // '28rem', 'min(70vh, 42rem)' …
}
15

Example: Server-side (Ajax)

Every DataTable works like this: a search, filter, sort or page change is a Livewire (Ajax) request; the package adds it to the query() query as SQL, the query runs in the database and only the rows of the open page return to the browser. 5,000 records and 5 million records open at the same speed. Demo: https://demo.acunsoft.test/admin/tables/datatable/server-side

PHP
protected function query(): Builder
{
    return Order::query();   // search, filters, sorting and pagination are added to this
}

protected function filters(): array
{
    return [
        Filter::multiSelect('Status', 'status')->options(OrderStatus::options())->inToolbar(),
        Filter::dateRange('Order date', 'ordered_at'),
    ];
}
16

Example: Fixed header

While the page scrolls, the header row stays below the top bar; at the end of the table it leaves together with the table. Sorting and "select all" work from the pinned header, in full screen and on phones too. When given together with scrollY(), scrollY() wins. Requires the PRO JS. Demo: https://demo.acunsoft.test/admin/tables/datatable/fixed-header

PHP
protected function fixedHeader(): bool
{
    return true;
}
17

Example: Fixed columns

While the table scrolls sideways, start keeps the selection, the (+) and the first n data columns on the left, and end keeps the actions column on the right; a shadow appears at the edge while content passes underneath. On phones (below 640 px) only the left ones are fixed. Can be used together with scrollX() and fixedHeader(). Requires the PRO JS. Demo: https://demo.acunsoft.test/admin/tables/datatable/fixed-columns

PHP
protected function fixedColumns(): array
{
    return ['start' => 1, 'end' => true];
}

protected function scrollX(): bool
{
    return true;
}
18

Example: Buttons (export)

CSV, JSON, PDF ("Save as PDF" from the print page), print and copy. The scope is chosen in the Export menu (this page, those matching the filters, the selected ones, all); the file is prepared on the server with the table's query, and large files are prepared in the queue and arrive with a download link. Demo: https://demo.acunsoft.test/admin/tables/datatable/buttons

PHP
protected function exportActions(): array
{
    return [ExportAction::csv(), ExportAction::pdf(), ExportAction::json(), ExportAction::print(), ExportAction::copy()];
}

protected function utilityActions(): array
{
    return [UtilityAction::refresh(), UtilityAction::columns()];
}
19

Configuration

General settings are in the config/acun-ui.php file, published with php artisan vendor:publish --tag=acun-ui-config. The page size can only be a value from the per_page list; any other value falls back to the default.

PHP
return [
    'datatable' => [
        'per_page' => [10, 25, 50, 100],
        'default_per_page' => 25,
        'search_debounce' => 400,      // ms
        'url_state' => true,           // search, sorting, filters, columns in the URL
        'sticky_header' => false,      // true: scrollY('min(70vh, 42rem)') on every table; per table, $stickyHeader
        'pagination' => 'paginate',    // paginate · simple · cursor

        'actions' => ['enabled' => true],
        'exports' => [
            'enabled' => true,
            'queue_threshold' => 5000,  // above this, a queued job
            'chunk_size' => 1000,
            'disk' => 'local',
            'directory' => 'exports',
            'queue' => null,
            'connection' => null,
            'expire_minutes' => 60,     // lifetime of the download link
            'poll_seconds' => 3,
            'cache_store' => null,      // cache shared with the worker
            'csv_delimiter' => ';',
            'copy_max_rows' => 1000,
        ],
        'print' => ['layout' => 'acun-ui-pro::components.data-table.print', 'max_rows' => 2000],   // PDF opens this page too
        'density' => ['default' => 'normal'],   // compact · normal · comfortable; a table changes it with density()
        'columns' => ['persist' => 'none'],                           // none · session
        'routes' => ['middleware' => ['web', 'auth'], 'prefix' => 'acun-ui/data-table'],
    ],
];
API

Props and slots

The values the component accepts.

PropDefaultDescription
$title''Title in the toolbar and accessible name of the table
$stickyHeaderconfig: falseOld name: same as scrollY('min(70vh, 42rem)')
scrollX()falseCells on a single line; a wide table scrolls sideways in its box (Column::wrap() columns wrap to the next line)
scrollY()nullBox height ('400px'): the table scrolls down inside the box, the header stays at the top
fixedHeader()falseWhile the page scrolls, the header row stays below the top bar; together with scrollY(), scrollY() wins (PRO JS)
fixedColumns()[]['start' => n, 'end' => true]: the selection, the (+) and the first n columns stay on the left, the actions on the right (PRO JS)
query() — Required. The base query; the record scope goes here
columns() — Required. List of Column
filters()[]List of Filter
headerActions()[]Toolbar actions (Actions\TableAction)
exportActions()[]Export menu (Actions\ExportAction)
utilityActions()[]Refresh, Columns, full screen, reset (Actions\UtilityAction); when empty, the column menu still shows
actions()[]Row actions (Actions\RowAction or the old Action)
bulkActions()[]Bulk actions (Actions\BulkAction or the old BulkAction)
defaultSort()null['field' => …, 'direction' => 'desc']: the order used until a column is chosen
density()config density.default ('normal')'compact' · 'normal' · 'comfortable': row density of the table
showsSearch()true when there is a field to searchSearch box; searchPlaceholder() gives its text
showsColumnPicker()null (automatic)false: no Columns button, true: always
showsPerPageSelector()truePage size box ("25")
perPageOptions() / defaultPerPage()config per_page / default_per_pageTable-specific page size options and the size on load
paginationOnEachSide()1Number of page numbers on each side of the current page
rowDetails()null'responsive' (only when a column is hidden) · 'always': row details window
paginationMode() / paginationView()config pagination / acun:paginationPagination type (paginate · simple · cursor) and its view
exportQuery(ExportScope) — Export query; override it for eager loading
authorizeTable() — Runs on the first load and on every later Livewire request
emptyState() — ['title' => …, 'description' => …]
buildQuery() — The query with search, filters and sorting applied (e.g. for export)
activeFilterCount(?bool $toolbar)nullNumber of active filters (a "0" value also counts); null: all, true: toolbar, false: panel
clearFilters(?bool $toolbar)nullClears the filters, resets to the first page and clears the selection; null: all, true: toolbar, false: panel
toggleFilterValue($key, $value) — Checks or unchecks an option in a multi-select filter; an undefined filter or option is ignored
URL state — q · sort · direction · filters · perPage · columns (when url_state is on)
Acun UI · DataTable engineDetailed usage and examples