Senren UI
Components / Data Table

Data Table

Stable Stimulus: senren--data-table

sortable

Customers

Customer subscriptions
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.