fabamb / laravel-tabulator
Server-side Tabulator.js integration for Laravel (remote pagination/sort/filter + Blade component)
Requires
- php: ^8.2
- illuminate/database: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-17 08:46:49 UTC
README
Server-side Tabulator.js integration for Laravel: a Blade component that renders the JS table, and a base class that handles remote pagination, sorting and filtering on the server.
See examples/ for a runnable-shaped snippet of every feature below.
Installation
Add the package as a composer path repository (until it's published):
{
"repositories": [
{"type": "path", "url": "../laravel-tabulator"}
],
"require": {
"fabamb/laravel-tabulator": "*"
}
}
composer require fabamb/laravel-tabulator
Publish the config (optional):
php artisan vendor:publish --tag=tabulator-config
Publish the language files to add a locale or edit strings (optional — en and it ship built in):
php artisan vendor:publish --tag=tabulator-lang
Tabulator's JS/CSS assets are not bundled by this package — include them yourself (CDN or your own build) before the component's <script> runs.
Basic usage: remote data source
- Define a table class extending
Fabamb\LaravelTabulator\TabulatorTable:
namespace App\Tabulator; use App\Models\User; use Fabamb\LaravelTabulator\TabulatorTable; use Illuminate\Database\Eloquent\Builder; class UserTabulatorTable extends TabulatorTable { public function query(): Builder { return User::query()->select('id', 'name', 'email', 'created_at'); } }
- Expose it behind a route:
Route::get('/users/data', function (App\Tabulator\UserTabulatorTable $table, Illuminate\Http\Request $request) { return $table->toResponse($request); })->name('users.data');
- Render the component in a Blade view:
<x-tabulator-table ajax-url="{{ route('users.data') }}" :columns="[ ['field' => 'id', 'title' => 'ID', 'width' => 80], ['field' => 'name', 'title' => 'Name'], ['field' => 'email', 'title' => 'Email'], ['field' => 'created_at', 'title' => 'Created at'], ]" rownum selectable="checkbox" />
Full example: examples/remote-basic/.
Non-Eloquent data source
query() isn't the only way to feed a table. When the data doesn't come from the database (a filesystem scan, an API response, anything already reduced to a plain array/Collection), call of() with a Collection instead of overriding query(). toResponse() then filters/sorts/paginates that collection in-memory and never calls query():
class LogFileTabulatorTable extends TabulatorTable { protected function searchableFields(): array { return ['name']; } }
Route::get('/logs/data', function (Request $request) { $files = collect(Storage::disk('logs')->files())->map(fn (string $path) => [ 'id' => $path, 'name' => basename($path), 'size' => Storage::disk('logs')->size($path), ]); return (new LogFileTabulatorTable())->of($files)->toResponse($request); })->name('logs.data');
Filter types, __global/searchableFields(), sorting and the size=true "all rows" case all behave the same as the query-backed path — field.dot relation filtering is the one exception, it's SQL-only (whereHas) and has no in-memory equivalent. transformer() and addScope() are unaffected by the choice; scopes just don't apply when the collection path is used, since there's no query to scope.
Full example: examples/collection-source/.
Server-side column definitions
Columns are normally passed inline via :columns (they're just Tabulator's own JS config). When the same table's columns are reused across multiple views (index, export, ...) and duplicating the array risks drift, override columns() on the table class instead — same idea as transformer():
class UserTabulatorTable extends TabulatorTable { public function query(): Builder { return User::query()->select('id', 'name', 'email', 'created_at'); } public function columns(): array { return [ ['field' => 'id', 'title' => 'ID', 'width' => 80], ['field' => 'name', 'title' => 'Name'], ['field' => 'email', 'title' => 'Email'], ['field' => 'created_at', 'title' => 'Created at'], ]; } }
Pass the table instance as :table and omit :columns to use it:
<x-tabulator-table ajax-url="{{ route('users.data') }}" :table="$table" rownum selectable="checkbox" actions="userActionsFormatter" />
An explicit :columns still overrides :table's, per view.
Full example: examples/server-columns/.
Server-side toolbar definitions
Same idea as columns(), for tables reused across multiple views that would otherwise repeat the same :toolbar array everywhere. Three ways to define a toolbar, pick whichever fits:
-
Local, view-only — the usual
:toolbar="[...]"array, or baretoolbar(usesToolbar::default()). Nothing to override. -
Server-side, extending the default — override
toolbar()on the table class, starting fromparent::toolbar():class UserTabulatorTable extends TabulatorTable { public function toolbar(): array { return parent::toolbar() + [ 'export-pdf' => ['icon' => 'fas fa-file-pdf', 'title' => 'Export PDF', 'url' => route('users.export-pdf')], ]; } }
-
Server-side, fully custom — override
toolbar()without callingparent::toolbar(), returning only what that table needs.
Pass :table with bare toolbar to use whichever toolbar() the table defines:
<x-tabulator-table ajax-url="{{ route('users.data') }}" :table="$table" toolbar />
An explicit :toolbar array on the view still overrides :table's, same precedence as :columns. Toolbar::default() also remains callable directly from the view at any time (e.g. :toolbar="\Fabamb\LaravelTabulator\Toolbar::default() + [...]"), :table doesn't take that away.
Full example: examples/server-toolbar/.
Local data (no server round-trip)
Pass data instead of ajax-url for a client-side table (pagination/sort/filter all happen in the browser):
<x-tabulator-table :data="$rows" :columns="[['field' => 'name', 'title' => 'Name']]" />
Full example: examples/local-data/.
Component props
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | auto-generated | DOM id of the table container |
columns |
array | [] |
Tabulator column definitions, passed through as-is. Overrides table's columns() when both are set |
table |
TabulatorTable|null |
null |
Table instance to pull columns() from when columns isn't passed (see Server-side column definitions), and toolbar() from when bare toolbar is passed (see Server-side toolbar definitions) |
ajax-url |
string|null | null |
Enables remote mode (pagination/sort/filter sent to the server) |
data |
array|null | null |
Local dataset, used when ajax-url is not set |
selectable |
bool|string | false |
Row selection. Bare selectable (true) is shorthand for 'both'. 'checkbox' — tickbox column only, clicking elsewhere in the row does nothing. 'click' — clicking anywhere in the row selects it, no column. 'both' — both at once |
rownum |
bool | false |
Adds a row-number column (frozen, not sortable) |
actions |
string|null | null |
Formatter name for a trailing actions column (frozen, not sortable, never collapsed by responsive) — e.g. actions="userActionsFormatter" |
responsive |
bool | false |
Shorthand for :options="['layout' => config('tabulator.responsive_fixed_layout'), 'responsiveLayout' => 'collapse']" (see Configuration reference); an explicit layout/responsiveLayout in options still wins |
search |
bool | false |
Adds a global search box above the table (see below) |
search-value |
string|null | null |
Pre-fills the search box and applies it as Tabulator's initialFilter on load (e.g. from a navbar search redirect). Implies search. |
options |
array | [] |
Raw Tabulator options, merged last — overrides anything the component computed |
toolbar |
array|bool | [] |
Buttons shown above the table (see Toolbar buttons). Bare toolbar (true) uses table's toolbar() when table is set, otherwise the standard Toolbar::default() set. Overrides table's toolbar() when both are set |
options is an escape hatch for any Tabulator setting not covered by a dedicated prop:
:options="['layout' => 'fitDataStretch', 'paginationSize' => 25, 'placeholder' => 'No matching rows']"
Full example: examples/options-override/.
Toolbar buttons
toolbar renders a button group above the table. Four standard actions are built in — no JS to write. Their tooltips (title) default to resources/lang/{locale}/tabulator.php (toolbar.* keys) unless a button sets its own title:
| key | action |
|---|---|
reload |
table.setData() |
csv |
table.download('csv', 'export.csv') |
print |
table.print() |
reset |
table.clearFilter(true); table.clearHeaderFilter() |
<x-tabulator-table ajax-url="{{ route('servers.data') }}" :toolbar="[ 'reload' => ['icon' => 'fas fa-sync', 'title' => 'Reload'], 'csv' => ['icon' => 'fas fa-file-csv', 'title' => 'Export CSV'], ]" :columns="[...]" />
Buttons are icon-only by default. Add label to a button to also show text next to the icon; it combines with title (the tooltip) by one rule:
label |
title |
Visible label | Tooltip |
|---|---|---|---|
| set | set | label |
title |
| set | not set | label |
label |
| not set | set | (none) | title |
| not set | not set | (none) | (none) |
:toolbar="[
'reload' => ['icon' => 'fas fa-sync', 'label' => 'Reload'],
]"
For app-specific actions (create, bulk-delete, ...), define a window.tabulatorButtons registry in your own JS — the component looks up any key it doesn't recognize as standard:
// resources/js/app.js window.tabulatorButtons = { 'bulk-delete': { action: (table, selected, url) => { if (!selected.length) return alert('Nothing selected'); // ... DELETE selected.map(r => r.id) to `url` }, }, };
:toolbar="['bulk-delete' => ['icon' => 'fas fa-trash-alt', 'url' => route('servers.bulk.destroy'), 'title' => 'Delete']]"
The click dispatch (button → table instance → action(table, selectedRows, url)) is registered once per page via @pushOnce, regardless of how many <x-tabulator-table> instances are on it.
Add a visual gap between buttons with a ['separator' => true] entry anywhere in the :toolbar array (its key doesn't matter, only that key is unique):
:toolbar="[
'reload' => ['icon' => 'fas fa-sync'],
'sep' => ['separator' => true],
'csv' => ['icon' => 'fas fa-file-csv'],
]"
Reusable bulk-action factory
bulk-actions.js ships Tabulator.bulkAction(options), covering the two common window.tabulatorButtons patterns above — confirm-then-fetch and prompt-then-fetch against the current selection — so you don't hand-roll the same fetch/confirm/prompt wiring per button. Publish it the same way as row-actions.js (see "Reusable factory" above), then:
window.tabulatorButtons = { 'bulk-delete': Tabulator.bulkAction({ method: 'DELETE', confirm: 'Delete selected rows?' }), 'bulk-tags': Tabulator.bulkAction({ method: 'PUT', prompt: 'Tags, comma separated:', bodyKey: 'tags', }), };
method is required. confirm/prompt are optional; a cancelled prompt() aborts the action. bodyKey (default 'value') names the field the prompted value is sent under, alongside ids (the selected rows' ids). empty overrides the alert shown when nothing is selected. Actions that need more than confirm/prompt — a modal site-picker, for instance — are still hand-written against the same window.tabulatorButtons registry.
Standard toolbar across many tables
An admin panel with several tables (users, servers, videos, ...) tends to repeat the same reload/csv/print/reset set everywhere, only the create (and, for selectable tables, bulk-edit/bulk-delete) URLs changing. Fabamb\LaravelTabulator\Toolbar::default() returns that set — no subclassing needed:
:toolbar="\Fabamb\LaravelTabulator\Toolbar::default(route('users.create'))"
When no create/bulk URL is needed at all, the bare toolbar attribute (Blade resolves the valueless attribute to true) is shorthand for the same standard set with no arguments:
<x-tabulator-table ajax-url="{{ route('users.data') }}" toolbar :columns="[...]" />
Omit the URL (or pass null) to drop the create button, e.g. for a read-only table. Icons (and, optionally, per-button class for color) come from config('tabulator.default_toolbar_icons') (Bootstrap Icons + Bootstrap colors by default) — override there project-wide, no code change needed. Titles still come from resources/lang/{locale}/tabulator.php.
Pass withLabels: true to also show each button's translated label next to its icon — a short toolbar_label.* string (falling back to the toolbar.* tooltip text for any key without one):
:toolbar="\Fabamb\LaravelTabulator\Toolbar::default(route('users.create'), withLabels: true)"
When create/bulk-edit/bulk-delete are present, a spacer (toolbar_separator_class) is inserted automatically before the standard reload/csv/print/reset set.
For tables with row selection (selectable), pass bulk-action URLs too:
<x-tabulator-table selectable="checkbox" :toolbar="\Fabamb\LaravelTabulator\Toolbar::default( createUrl: route('users.create'), bulkDeleteUrl: route('users.bulk.destroy'), bulkEditUrl: route('users.bulk.edit'), )" ... />
bulk-edit/bulk-delete are omitted (like create) when their URL is null. They rely on the same window.tabulatorButtons action wiring as any other button with a url — see Toolbar buttons above.
Toolbar::default() returns a plain array, so adding an app-specific button alongside the standard set needs no helper — just + a custom entry onto it (a key colliding with a standard one, e.g. reload, loses to the standard button, since + keeps the left array's value on conflict):
:toolbar="\Fabamb\LaravelTabulator\Toolbar::default(route('users.create')) + [
'export-pdf' => ['icon' => 'fas fa-file-pdf', 'title' => 'Export PDF', 'url' => route('users.export-pdf')],
]"
Full example: examples/toolbar/.
Row actions
A per-row action column (View/Edit/Delete, or anything else) is a plain Tabulator column with no field and a custom formatter, declared in :columns like any other column:
:columns="[
['formatter' => 'rowActionButtons', 'title' => '', 'width' => 100, 'headerSort' => false, 'hozAlign' => 'center'],
['field' => 'name', 'title' => 'Name'],
]"
The actions prop is shorthand for exactly that column (frozen, not sortable, not resizable, never collapsed by responsive), appended at the end of columns:
<x-tabulator-table :columns="[['field' => 'name', 'title' => 'Name']]" actions="rowActionButtons" />
formatter as a string is a lookup against Tabulator's own formatter registry (built-ins: plaintext, html, money, ...) — not window[name] — so it survives the :columns array's PHP-array → @json() → JS-object trip as-is; no package-specific resolution needed. Register the custom formatter once, before any new Tabulator(...) call:
Push order matters.
<x-tabulator-table>itself does a@push(config('tabulator.stack'))containing thenew Tabulator(...)call, at the exact point the component tag appears in your view. Tabulator resolves a stringformatteragainst its registry synchronously at that construction call — if the name isn't registered yet, it warnsFormatter Error - No such formatter foundand permanently falls back toplaintextfor that column (no retry later).@stackrenders all pushed<script>blocks in one place, but in the order they were pushed, so your own@push('js')registering the formatter must appear before the<x-tabulator-table>tag in the source, not after.
// resources/js/tabulator-formatters.js, imported by resources/js/app.js. Tabulator.extendModule('format', 'formatters', { rowActionButtons: function (cell) { const id = cell.getData().id; const wrap = document.createElement('div'); wrap.innerHTML = `<a href="/users/${id}/edit" class="btn btn-sm btn-primary">Edit</a>`; return wrap; }, });
The formatter receives the Tabulator cell; cell.getData() is the full row, so it needs no server-side transformer — this is a purely client-side column. If a row action instead needs data the row doesn't already carry (a computed field, a related model not otherwise shown), reshape the row with transformer() and read the extra key from cell.getData() the same way.
A dropdown-menu formatter (kebab button + Bootstrap 5 .dropdown-menu) needs two extra lines: Tabulator rows are overflow: hidden and transformed (virtual scroll), which clips a normally-positioned Popper dropdown and traps it in that row's own stacking context. Fix: strategy: 'fixed' on the Popper config, and move the menu to document.body only while open (back to the cell on close, so redraws don't leak detached nodes) — see formatters.js in the full example below.
Full example: examples/row-actions/.
Reusable factory
Hand-rolling the formatter above per resource repeats the same DOM/URL/dropdown wiring. resources/js/row-actions.js ships two configurable factories, Tabulator.rowActionButtons(actions) and Tabulator.rowActionKebab(actions), that build the formatter for you from an actions config — no assumed action set, no baked-in labels/icons/colors, no i18n library.
Publish it once:
php artisan vendor:publish --tag=tabulator-js
This copies it to public/vendor/tabulator/row-actions.js — a plain global (Tabulator.rowActionButtons/Tabulator.rowActionKebab), no bundler required. Load it with a <script> tag (e.g. via config('adminlte.plugins'), or any other global-asset mechanism), or import it from your own resources/js/app.js if you do run a bundler:
import '/vendor/tabulator/row-actions.js';
Then, per table, define the actions config and register the formatter — same Tabulator.extendModule call as any custom formatter (same push-order rule as above: this @push('js') block must come before <x-tabulator-table>):
@push('js') <script> const userActions = { view: { url: id => `/users/${id}`, icon: 'bi-eye' }, edit: { url: id => `/users/${id}/edit`, icon: 'bi-pencil' }, delete: { url: id => `/users/${id}`, icon: 'bi-trash', method: 'DELETE', class: 'text-danger', label: '{{ __('Delete') }}', confirm: '{{ __('Delete this row?') }}', }, }; Tabulator.extendModule('format', 'formatters', { userActionButtons: Tabulator.rowActionButtons(userActions), userActionKebab: Tabulator.rowActionKebab(userActions), }); </script> @endpush
:columns="[
['formatter' => 'userActionButtons', 'title' => '', 'width' => 120, 'headerSort' => false, 'hozAlign' => 'center'],
// ...
]"
Each action key is free-form (not assumed to be view/edit/delete): url(id) (required), icon (required, a Bootstrap Icons class), plus optional class, label, method (omitted/GET renders an <a href>; anything else renders a <button> that fetches and refreshes the table), and confirm.
Labels are plain strings the config provides — row-actions.js has no i18n of its own and never touches this package's own tabulator.php lang file, which is reserved for Tabulator's built-in UI strings (toolbar, pagination, ...). Since the actions config is written in your own @push('js') block, it's rendered by Blade like any other view — use your own __() calls and your own lang file there, same as for any other string in your app.
Full example: examples/row-actions-factory/.
Column filters
Column-level filters are native Tabulator, nothing package-specific: set headerFilter on any column.
:columns="[
['field' => 'name', 'title' => 'Name', 'headerFilter' => 'input'],
['field' => 'status', 'title' => 'Status', 'headerFilter' => 'list', 'headerFilterParams' => ['values' => ['active' => 'Active', 'inactive' => 'Inactive']]],
]"
A column without headerFilter never shows a filter box — there's no separate "enable filters" flag to configure.
In remote mode, filters are sent to the server as {field, type, value} tuples and applied by TabulatorTable::applyFilters(). Supported types: = (default), like, in, <, <=, >, >=.
Full example: examples/column-filters/.
Relation columns
A column can show data from a belongsTo (or any) relation using a dotted field, e.g. role.name. Two things to set up in the TabulatorTable:
public function query(): Builder { // Eager-load, or it's an N+1 per row. return User::query()->select('id', 'name', 'role_id')->with('role'); } protected function transformer(): ?callable { return fn (User $user) => [ 'id' => $user->id, 'name' => $user->name, // Nested, not a flat 'role.name' key: Tabulator's dotted `field` // reads it as a path into a nested object. 'role' => ['name' => $user->role?->name], ]; }
['field' => 'role.name', 'title' => 'Role', 'headerFilter' => 'input']
headerFilter on a dotted field works out of the box: applyFilters() detects the dot and filters via whereHas($relation, ...) instead of a plain where(). Sorting on a dotted field is not supported — orderBy needs a join, which the package doesn't set up automatically; add one manually in query() if you need it.
Full example: examples/relations/.
Scopes
Register extra query constraints per-request (e.g. tenant, session state) without hardcoding them in query(). Uses native Illuminate\Database\Eloquent\Scope, nothing package-specific:
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; class ByStatusScope implements Scope { public function __construct(private readonly string $status) {} public function apply(Builder $builder, Model $model): void { $builder->where('status', $this->status); } }
Add scopes from the controller, they accumulate and are all applied before filter/sort/pagination:
$table = new UserTabulatorTable(); $table->addScope(new ByStatusScope($request->get('status', 'active'))); $table->addScope(new ByTenantScope($request->user()->tenant_id)); return $table->toResponse($request);
Full example: examples/scopes/.
Transformers
Reshape each row before it's sent as JSON — e.g. rendering a badge, formatting a date, dropping a column that shouldn't reach the client. Override transformer() on your table class to return a callable; it receives the model and returns the array sent to Tabulator (keys map to column fields):
class UserTabulatorTable extends TabulatorTable { protected function transformer(): ?callable { return fn (User $user) => [ 'id' => $user->id, 'name' => $user->name, 'status' => view('components.badge', ['status' => $user->status])->render(), ]; } }
Applied only to the current page's rows (or all rows when size is All), after pagination — not the whole dataset. null (the default) means no transformation, rows pass through as returned by query(). No dependency on a specific transformer library (e.g. Fractal) — any callable works, including [$instance, 'method'] for transformers with constructor dependencies.
Full example: examples/transformers/.
Global search
Set search on the component to render a search box above the table:
<x-tabulator-table ajax-url="{{ route('users.data') }}" :columns="[...]" search />
In remote mode, override searchableFields() on your table class to say which columns the search box matches (OR'd together with like). Without it, the search box is a no-op:
class UserTabulatorTable extends TabulatorTable { protected function searchableFields(): array { return ['name', 'email']; } }
Full example: examples/global-search/.
In local mode, search uses Tabulator's own setFilter against the in-browser dataset — no server-side wiring needed.
Debounce and minimum character count are configurable in config/tabulator.php:
'search_debounce_ms' => 300, 'search_min_chars' => 2, 'search_icon' => 'bi bi-search',
The search box's placeholder comes from resources/lang/{locale}/tabulator.php's search_placeholder key, same as toolbar tooltips.
Search from outside the table (e.g. navbar)
Pass search-value to prefill the box and filter on first load, sourced from wherever the request came from (typically a query string):
<x-tabulator-table ajax-url="{{ route('users.data') }}" :columns="[...]" search :search-value="request('search')" />
Pattern for a navbar search box that isn't on the table's page: submit a plain GET form to the table's route with a search field (AdminLTE's built-in navbar-search menu item works — set method => 'get', input_name => 'search', url => '/your-route'). The component drops the query string via history.replaceState right after applying the initial filter, so a later reload starts unfiltered instead of being stuck on the old search.
This only targets one table/route per navbar search box — for search across multiple unrelated tables, route selection is up to the caller (not handled by this package).
Full example: examples/navbar-search/.
Full example: examples/localization/.
Configuration reference (config/tabulator.php)
| Key | Description |
|---|---|
stack |
Blade @push stack the component's <script> goes into (must match a @stack in your layout) |
layout |
Tabulator layout option (default fitColumns) |
movable_columns |
Tabulator movableColumns option (default true) |
responsive_layout |
Tabulator responsiveLayout option (default false); set to 'collapse' to hide columns that no longer fit behind a per-row expand arrow (the component adds the responsiveCollapse toggle column for it automatically — coexists fine with selectable's own column), or 'hide' to just drop them. Requires layout to be a fixed-width mode (fitData/fitDataFill/fitDataStretch) — the default fitColumns shrinks every column to always fit instead of ever overflowing, so collapse/hide never trigger. The responsive prop below sets both together automatically |
responsive_fixed_layout |
layout used when the responsive prop is set (default fitDataFill) |
pagination_size |
Default page size |
pagination_size_selector |
Options in the page-size dropdown |
pagination_counter |
Tabulator paginationCounter value |
selectable_width |
Width (px) of the row-selection checkbox column |
selectable_frozen |
Whether the row-selection checkbox column stays pinned while scrolling horizontally |
rownum_width |
Width (px) of the row-number column |
actions_width |
Width (px) of the actions prop's trailing column |
actions_frozen |
Whether the actions prop's trailing column stays pinned while scrolling horizontally |
locale |
Active locale, used to load resources/lang/{locale}/tabulator.php for both Tabulator's own UI strings and toolbar tooltips (en/it ship built in); set to false to keep Tabulator's built-in English |
toolbar_button_class |
Default Bootstrap class for toolbar buttons (default btn-secondary); override per button with class |
toolbar_button_size_class |
Bootstrap size class for toolbar buttons |
toolbar_separator_class |
Class for the gap rendered by a ['separator' => true] toolbar entry (default mx-1, plain spacing — set to vr mx-1 for a visible vertical rule instead) |
default_toolbar_icons |
Icon, class (button color, added on top of toolbar_button_class, e.g. bg-primary) and icon_class (icon color, needed on bg-body-secondary buttons for contrast — use theme-adaptive text-* utilities so it stays legible when data-bs-theme switches to dark) used by Toolbar::default() per button, e.g. ['icon' => 'bi bi-plus-lg', 'class' => 'bg-primary'] |
search_size_class |
Bootstrap size class for the search box's input group |
search_width |
Max width (CSS value) of the search box |
search_debounce_ms / search_min_chars |
Global search box behavior |
search_icon |
CSS class of the icon shown in the search box (default bi bi-search, Bootstrap Icons) |
Testing
vendor/bin/phpunit tests