Search documentation

Find a component or a docs page

Data Table

A Hotwire-first data table. Every interaction (sort, search, pagination) is a Rails request answered with HTML, swapped via Turbo Frame. Row selection uses form-first submission.

Complete demo

Full feature set — search, sort, first/previous/next/last pagination, per-page, select-all, row checkboxes, bulk actions, row actions dropdown, column visibility, badge cells.

Complete demo

Server-driven

Turbo Frame GET on each sort/search/page. No client-only state.

Server-driven

Selection + bulk actions

DataTableBulkActions is a plain slot — put any Phlex content inside. Row checkboxes are <input name="ids[]"> elements inside DataTableForm. Bulk action buttons submit that form with the selected IDs via HTML5 form-association attributes.

Selection + bulk actions

Bulk action button attributes

Because the submit buttons live inside DataTableToolbar (outside DataTableForm), you must use HTML5 form-association attributes to wire them up. Server receives params[:ids] as an array.

AttributeRequiredPurpose
type: "submit"yesnative submit button
form: FORM_IDyes (button is outside DataTableForm)HTML5 form-association — lets the button submit a form located elsewhere in the DOM
formaction: "/path"yestarget URL, overrides the form's action
formmethod: "post"yesHTTP verb, overrides the form's method
formnovalidate: trueoptionalskip HTML5 validation
data: {turbo_confirm: "Are you sure?"}optionalRails/Turbo confirmation dialog before submit

button_tobutton_to "Delete", path, method: :delete, form: {data: {turbo_confirm: "..."}}

Rails controller example

Your endpoint receives the selected IDs as params[:ids] (an array of strings):

class EmployeesController < ApplicationController
  def bulk_delete
    ids = Array(params[:ids]).map(&:to_i)
    Employee.where(id: ids).destroy_all
    redirect_to employees_path, notice: "Deleted #{ids.size} employees"
  end

  def bulk_export
    ids = Array(params[:ids]).map(&:to_i)
    employees = Employee.where(id: ids)
    send_data employees.to_csv, filename: "employees.csv"
  end
end

Column visibility

Client-side toggle. Hidden columns get `hidden` class via data-column attribute matching.

Column visibility is client-side and resets on every Turbo Frame swap (sort/search/page re-renders). If you need it to persist, encode it in a URL param (e.g. `?columns=name,status`) or store in localStorage.

Column visibility

Custom cell renderers

Plain Ruby helpers for badge/date/currency — the gem does not ship renderers.

Custom cell renderers

Expandable rows

Toggle a detail region below each row. Accessible: aria-expanded, aria-controls, keyboard-focusable button, region role on the expanded content.

Expandable rows

Pagination adapters

DataTablePagination accepts a pagination source via one of four keyword forms. Each resolves to an internal adapter exposing current_page, total_pages, total_count, and per_page.

Manual

No gem required. Pass page/per_page/total_count directly.

DataTablePagination(
  page: @page,
  per_page: @per_page,
  total_count: @total_count,
  path: employees_path
)

Pagy

If you use Pagy, pass the pagy object directly.

@pagy, @employees = pagy(Employee.all)

DataTablePagination(pagy: @pagy, path: employees_path)

Kaminari

If you use Kaminari, pass the paginated collection.

@employees = Employee.page(params[:page]).per(25)

DataTablePagination(kaminari: @employees, path: employees_path)

Custom adapter

Any object responding to current_page, total_pages, total_count and per_page works via the with: keyword. Useful when wrapping a different gem or custom pagination logic.

class MyAdapter
  def initialize(result)
    @result = result
  end

  def current_page = @result.page
  def total_pages  = @result.total_pages
  def total_count  = @result.count
  def per_page     = @result.limit
end

DataTablePagination(with: MyAdapter.new(@result), path: employees_path)