The Data Table Problem in Laravel

If you have built Laravel applications with content-heavy admin panels or internal tools, you know the data table problem well. At some point, every application needs to display tabular data that users can sort, filter, and paginate. The options are not great:

Livewire DataTables — full-featured, but adds a Livewire component layer to what is fundamentally a display concern. Fine for complex interactive UIs, overkill for simple tables.

Spatie Laravel Query Builder — excellent for API endpoints, but building the HTML table on top of it still requires custom Blade work for every column definition.

Vue or React DataGrid components — excellent UX, but introduces a JavaScript framework dependency for a UI element that does not need reactive state management.

Filament Tables — fantastic within Filament panels, but not portable to standard Blade views.

Custom Blade tables — fast to start, quickly becomes unmaintainable when sorting, searching, and pagination need to coexist.

Laravel Grid.js occupies a gap in this landscape: a PHP-first, Eloquent-native abstraction over Grid.js — a 30KB vanilla JavaScript table library with no framework dependencies.

Why Grid.js Specifically

I chose Grid.js after evaluating several JavaScript data table libraries:

  • DataTables — battle-tested, but jQuery-dependent and visually dated without significant CSS customisation
  • AG Grid — extremely powerful, but the free version has meaningful limitations and the bundle size is significant
  • TanStack Table — excellent, but designed for React/Vue component trees, not server-rendered HTML integration
  • Grid.js — zero dependencies, clean API, designed for server-side data, actively maintained, 4KB CSS footprint

Grid.js's server-side pagination mode was the deciding factor. It handles all the UI state (current page, sort column, sort direction, search query) and sends AJAX requests to a configurable endpoint. The server just needs to respond with paginated data in a defined JSON format. This maps perfectly to an Eloquent-backed controller action.

The PHP API Design

The core design principle was that the PHP API should feel native to Laravel. Column definitions should look like Eloquent attribute access. Sorting should feel like calling ->orderBy(). The result should be a clean JSON response from a standard controller.

use AmjadIqbal\LaravelGridjs\GridBuilder;
use AmjadIqbal\LaravelGridjs\Column;

class UserController extends Controller
{
    public function index(): View
    {
        $grid = GridBuilder::make()
            ->query(User::query()->withCount('posts'))
            ->columns([
                Column::make('id', '#')->sortable()->width(60),
                Column::make('name', 'Name')->sortable()->searchable(),
                Column::make('email', 'Email')->searchable(),
                Column::make('posts_count', 'Posts')->sortable(),
                Column::make('created_at', 'Joined')
                    ->sortable()
                    ->format(fn ($val) => Carbon::parse($val)->diffForHumans()),
            ])
            ->defaultSort('created_at', 'desc')
            ->perPage(25);

        return view('users.index', compact('grid'));
    }

    public function data(Request $request): JsonResponse
    {
        return GridBuilder::make()
            ->query(User::query()->withCount('posts'))
            ->columns([...])
            ->respond($request);
    }
}

In the Blade view:

@gridjs($grid)

One directive, one controller method, a fully functional sortable, searchable, paginated data table.

The Server-Side Response Contract

Grid.js expects paginated AJAX responses in a specific format:

{
    "data": [
        { "id": 1, "name": "Amjad", "email": "[email protected]" }
    ],
    "total": 142
}

The GridBuilder::respond() method handles this by applying the request's sort, order, search, and page query parameters to the Eloquent query and returning the formatted JsonResponse.

The sort safety is worth mentioning: to prevent SQL injection through the sort column parameter, the respond() method validates the sort column against the registered column definitions before applying it to the query. Unrecognised column names are ignored.

Performance Considerations

Server-side pagination means only the current page of data is fetched from the database per request — unlike client-side tables that load the entire dataset upfront. For tables with thousands or millions of rows, this is a meaningful difference.

The search implementation uses a simple LIKE %search% approach by default, which is appropriate for most use cases but will be slow on large unindexed string columns. The package documents this and provides a hook for replacing the default search with a full-text search implementation (Laravel Scout, Meilisearch, etc.).

GitHub Repository

Source code, documentation, and full API reference available on GitHub. Install via Composer:

composer require amjadiqbal/laravel-gridjs

Conclusion

Laravel Grid.js is built for one specific situation: you need an interactive, paginated, sortable data table in a Laravel Blade application, and you do not want to introduce a JavaScript framework dependency to get it. If that describes your situation, the package is worth trying. If you need a full reactive component with inline editing and complex state management, Livewire DataTables or Filament Tables are the better fit.

The right tool for the right job — always.