Data Table
Stable Stimulus:senren--data-table
sortable
Customers
| Northstar | Pro | $4,200 | Active |
| Orbit | Team | $1,860 | Trial |
| Garden Co. | Starter | $640 | Active |
Showing 3 of 42 customers
Usage example #
Copy this ERB into a Rails view after installing the component. The snippet below is the same code used by the live preview above.
app/views/.../data_table_example.html.erb
<div class="w-full max-w-2xl">
<%= render Senren::DataTableComponent.new(
caption: "Customer subscriptions",
columns: [
{ key: :name, label: "Customer" },
{ key: :plan, label: "Plan" },
{ key: :mrr, label: "MRR" },
{ key: :status, label: "Status" }
],
rows: [
{ name: "Northstar", plan: "Pro", mrr: "$4,200", status: "Active" },
{ name: "Orbit", plan: "Team", mrr: "$1,860", status: "Trial" },
{ name: "Garden Co.", plan: "Starter", mrr: "$640", status: "Active" }
]
) do |table| %>
<% table.with_toolbar do %>
<div class="flex items-center justify-between gap-3">
<p class="text-sm font-medium text-[hsl(var(--senren-foreground))]">Customers</p>
<%= render Senren::SearchInputComponent.new(name: "customers", placeholder: "Search customers...") %>
</div>
<% end %>
<% table.with_footer do %>
<p class="text-xs text-[hsl(var(--senren-muted-foreground))]">Showing 3 of 42 customers</p>
<% end %>
<% end %>
</div>
Install this component #
Copy the official component into your app
This component requires Stimulus. Keep --client so the controller is copied with the ViewComponent.
Terminal
bin/rails senren:add data_table --client
Create a custom component with the same conventions
Use this when you need an app-specific component that follows Senren's ViewComponent and Stimulus structure. --client is required for this behavior.
Terminal
bin/rails generate senren:component data_table --client
Dependencies are resolved by senren:add:
table, dropdown_menu, pagination.
At a glance #
Category
Saas
Class name
Senren::DataTableComponent
Stimulus
senren--data-table
Variants
default
Depends on
table, dropdown_menu, pagination
Pairs with
filter_bar, search_input
Source #
app/components/senren/data_table_component.html.erb
<%= tag.div(**root_attrs("overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] shadow-sm", data: { controller: "senren--data-table" })) do %>
<% if toolbar? %>
<div class="border-b border-[hsl(var(--senren-border))] p-3"><%= toolbar %></div>
<% end %>
<% if content? && columns.empty? %>
<div class="p-3"><%= content %></div>
<% else %>
<div class="overflow-x-auto">
<table class="w-full border-collapse text-left text-sm">
<% if caption.present? %>
<caption class="sr-only"><%= caption %></caption>
<% end %>
<thead class="bg-[hsl(var(--senren-muted)/0.6)] text-[hsl(var(--senren-muted-foreground))]">
<tr>
<% columns.each do |column| %>
<th scope="col" class="px-4 py-3 font-medium">
<% if sortable? %>
<button type="button" class="inline-flex cursor-pointer items-center gap-1 rounded-sm hover:text-[hsl(var(--senren-foreground))]" data-action="click->senren--data-table#sort" data-sort-key="<%= column_sort_key(column) %>">
<%= column_label(column) %>
<span aria-hidden="true">Sort</span>
</button>
<% else %>
<%= column_label(column) %>
<% end %>
</th>
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-[hsl(var(--senren-border))] text-[hsl(var(--senren-foreground))]" data-senren--data-table-target="body">
<% rows.each do |row| %>
<tr class="transition-colors hover:bg-[hsl(var(--senren-muted)/0.35)]" data-senren--data-table-target="row">
<% columns.each do |column| %>
<% value = cell_value(row, column) %>
<td class="px-4 py-3" data-sort-key="<%= column_sort_key(column) %>" data-sort-value="<%= value %>"><%= value %></td>
<% end %>
</tr>
<% end %>
<% if rows.empty? %>
<tr><td class="px-4 py-8 text-center text-[hsl(var(--senren-muted-foreground))]" colspan="<%= columns.size.nonzero? || 1 %>"><%= empty_text %></td></tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
<% if footer? %>
<div class="border-t border-[hsl(var(--senren-border))] p-3"><%= footer %></div>
<% end %>
<% end %>
app/components/senren/data_table_component.rb
module Senren
class DataTableComponent < BaseComponent
renders_one :toolbar
renders_one :footer
VARIANTS = { default: '' }.freeze
SIZES = { md: '' }.freeze
def initialize(columns: [], rows: [], caption: nil, empty_text: 'No records found.', sortable: true,
variant: :default, class_name: nil, **html)
super(variant: variant, size: :md, class_name: class_name, **html)
@columns = Array(columns)
@rows = Array(rows)
@caption = caption
@empty_text = empty_text
@sortable = sortable
end
attr_reader :columns, :rows, :caption, :empty_text
def sortable? = !!@sortable
def cell_value(row, column)
key = column_key(column)
row.is_a?(Hash) ? (row[key] || row[key.to_s]) : row[key.to_i]
end
def column_label(column)
column.is_a?(Hash) ? (column[:label] || column['label']) : column.to_s.tr('_', ' ').capitalize
end
def column_sort_key(column)
column_key(column).to_s
end
private
def column_key(column)
column.is_a?(Hash) ? (column[:key] || column['key']) : column
end
end
end
AI agent rules #
Use for
- +sortable
- +filterable record lists
Avoid
- -static layout grids
Accessibility #
- Inherits from table; columns must have th/scope.