Rapid Rails UI Pro

Upgrade to unlock this component

Get Pro →

Table

Full-featured data table with sorting, row selection, pagination, and responsive layouts.

Key Features

  • Column Formatters - 10 built-in formatters (date, currency, boolean, badge, etc.)
  • Data-Driven Rows - Auto-generate cells from data objects
  • Toolbar Slot - Search, filters, and actions above the table
  • Sorting - Client-side or server-side column sorting
  • Row Selection - Checkbox selection with select-all
  • Bulk Actions - Action bar for selected rows
  • 5 Sizes - xs, sm, base, lg, xl
  • 2 Shapes - Rounded or square corners
  • Visual Variants - Striped rows, striped columns, bordered, hoverable
  • Visible Caption - Title and description header
  • Sticky Header - Fixed header when scrolling
  • 3 Responsive Modes - Scroll, cards, stack
  • Pagination - Pagy integration with live example
  • Empty State - Custom empty content slot
  • Loading State - Skeleton loading animation
  • Footer - Summary row support
  • Turbo Integration - Turbo Frame wrapper
  • Full Accessibility - ARIA grid role, keyboard navigation
  • JavaScript API - Programmatic control with data-action attributes

Basic Usage

Create a simple table with columns and rows using the slot-based API.

NameEmailRole
John Doejohn@example.comAdmin
Jane Smithjane@example.comEditor
Bob Johnsonbob@example.comViewer
Basic Table
<%= rui_table do |table| %>
  <% table.with_column(key: :name, label: "Name") %>
  <% table.with_column(key: :email, label: "Email") %>
  <% table.with_column(key: :role, label: "Role") %>

  <% table.with_row(id: 1) do |row| %>
    <% row.with_cell { "John Doe" } %>
    <% row.with_cell { "john@example.com" } %>
    <% row.with_cell { "Admin" } %>
  <% end %>

  <% table.with_row(id: 2) do |row| %>
    <% row.with_cell { "Jane Smith" } %>
    <% row.with_cell { "jane@example.com" } %>
    <% row.with_cell { "Editor" } %>
  <% end %>
<% end %>

Visible Caption

Add a title and description above the table for context. Unlike the accessibility caption slot, these are visible to all users.

Team Members

A list of all team members including their name, title, and email address.

NameTitleEmail
John DoeSoftware Engineerjohn@example.com
Jane SmithProduct Managerjane@example.com
Bob WilsonDesignerbob@example.com
Table with Caption
<%= rui_table(
  title: "Team Members",
  description: "A list of all team members including their name, title, and email.",
  bordered: true,
  shape: :rounded
  ) do |table| %>
    <% table.with_column(key: :name, label: "Name") %>
    <% table.with_column(key: :title, label: "Title") %>
    <% table.with_column(key: :email, label: "Email") %>
    ...
  <% end %>

  # Title only
  <%= rui_table(title: "Recent Orders") do |table| %>
    ...
  <% end %>

  # Description only
  <%= rui_table(description: "Showing orders from the last 30 days") do |table| %>
  ...
<% end %>

Columns

Configure columns with labels, alignment, width, and sortable options.

IDProductPriceIn Stock
001Wireless Keyboard$49.99125
002USB-C Hub$79.9942
Column Configuration
<% table.with_column(key: :id, label: "ID", width: "60px", align: :center) %>
<% table.with_column(key: :product, label: "Product") %>
<% table.with_column(key: :price, label: "Price", align: :right, width: "100px") %>
<% table.with_column(key: :stock, label: "In Stock", align: :center) %>

# Column options:
# - key: Symbol - Column identifier (used for sorting)
# - label: String - Header text (auto-generated from key if not provided)
# - sortable: Boolean - Enable sorting on this column
# - align: Symbol - :left, :center, :right
# - width: String - CSS width value (e.g., "100px", "20%")

Rows & Cells

Add rows with optional ID, data object, and color highlighting. Cells can contain any content.

StatusOrderCustomerTotal
Completed #ORD-001Alice Brown$299.00
Pending #ORD-002Charlie Davis$149.50
Cancelled #ORD-003David Wilson$89.00
Row Colors
# Highlight rows with semantic colors
<% table.with_row(id: 1, color: :success) do |row| %>
<% row.with_cell { rui_badge("Completed", color: :success) } %>
<% row.with_cell { "#ORD-001" } %>
<% end %>

<% table.with_row(id: 2, color: :warning) do |row| %>
<% row.with_cell { rui_badge("Pending", color: :warning) } %>
<% row.with_cell { "#ORD-002" } %>
<% end %>

<% table.with_row(id: 3, color: :danger) do |row| %>
<% row.with_cell { rui_badge("Cancelled", color: :danger) } %>
<% row.with_cell { "#ORD-003" } %>
<% end %>

# Row options:
# - id: Any - Row identifier (required for selection)
# - data: Hash/Object - Attach data to row
# - color: Symbol - Row highlight color

Column Formatters

Apply built-in formatters to columns for automatic data formatting. Supports dates, currency, percentages, booleans, and more.

Subscription Plans

Demonstrating all built-in formatters

Plan NamePriceDiscountStatusCreatedDescription
Premium Plan$99.990.2%3 days agoUnlimited access to all features...
Basic Plan$29.990.0%7 days agoEssential features for small teams...
Legacy Plan$49.990.2%2 months agoDeprecated plan no longer available...

Available Formatters

:date - Format as date (Jan 15, 2024)
:datetime - Format as date and time
:time - Format as time only (2:30 PM)
:relative_time - Time ago (3 days ago)
:currency - Format as currency ($1,234.56)
:number - Format with separators (1,234,567)
:percentage - Format as percentage (75.5%)
:boolean - Format as Yes/No or custom text
:truncate - Truncate with ellipsis
:badge - Render as colored badge
Using Column Formatters
# Simple formatter (symbol)
<% table.with_column(key: :price, label: "Price", formatter: :currency) %>
<% table.with_column(key: :created_at, label: "Created", formatter: :date) %>
<% table.with_column(key: :active, label: "Active", formatter: :boolean) %>

  # Formatter with options (hash)
<% table.with_column(
  key: :discount,
  label: "Discount",
  formatter: { type: :percentage, precision: 1 }
) %>

<% table.with_column(
  key: :status,
  label: "Status",
  formatter: { type: :boolean, true_text: "Active", false_text: "Inactive" }
) %>

<% table.with_column(
  key: :description,
  label: "Description",
  formatter: { type: :truncate, length: 50 }
) %>

# Badge formatter with color mapping
<% table.with_column(
  key: :status,
  label: "Status",
  formatter: {
    type: :badge,
    colors: { active: :success, pending: :warning, cancelled: :danger }
  }
) %>

# Custom formatter with Proc
<% table.with_column(
  key: :email,
  label: "Email",
  formatter: ->(value) { mail_to(value, value, class: "text-blue-600") }
) %>

Date & Time Formatters

EventDate (:date)Relative (:relative_time)Time (:time)
MeetingJan 09, 20262 days ago02:30 PM
DeadlineJan 18, 20267 days ago05:00 PM
ReviewJan 11, 2026less than a minute ago09:15 AM

Currency & Number Formatters

ItemAmountGrowthUnits
Revenue$1,234,567.890.2%45,678
Expenses$876,543.21-0.0%12,345
Profit$358,024.680.3%33,333

Badge Formatter

Order IDCustomerStatus
#ORD-001Alice Brown completed
#ORD-002Bob Wilson pending
#ORD-003Carol Davis cancelled
#ORD-004Dave Smith processing

Data-Driven Rows

Pass a data: object to rows for automatic cell generation. Column keys are used to extract values from the object. Combined with formatters, this creates a fully declarative table definition.

Users

Data-driven rows with automatic cell generation

NameEmailBalanceVerifiedJoined
Alice Johnsonalice@example.com$1,250.0014 days ago
Bob Smithbob@example.com$750.50about 1 month ago
Carol Williamscarol@example.com$0.003 days ago
Data-Driven Rows
# Define columns with keys that match your data object
      <%= rui_table do |table| %>
      <% table.with_column(key: :name, label: "Name") %>
      <% table.with_column(key: :email, label: "Email") %>
      <% table.with_column(key: :balance, label: "Balance", formatter: :currency) %>
      <% table.with_column(key: :verified, label: "Verified", formatter: :boolean) %>
      <% table.with_column(key: :created_at, label: "Joined", formatter: :relative_time) %>

      <%# Cells are auto-generated from data object %>
      <% @users.each do |user| %>
    <% table.with_row(id: user.id, data: user) %>
      <% end %>
      <% end %>

# Works with:
# - ActiveRecord models
# - Struct objects
# - Plain Ruby objects with methods
# - Hashes (with symbol or string keys)

Manual vs Data-Driven Comparison

Manual Cells (verbose)
<% @users.each do |user| %>
  <% table.with_row(id: user.id) do |row| %>
    <% row.with_cell { user.name } %>
    <% row.with_cell { user.email } %>
    <% row.with_cell { number_to_currency(user.balance) } %>
    <% row.with_cell { user.verified ? "Yes" : "No" } %>
    <% row.with_cell { time_ago_in_words(user.created_at) } %>
  <% end %>
<% end %>
Data-Driven (clean)
<% @users.each do |user| %>
  <% table.with_row(id: user.id, data: user) %>
<% end %>



<%# Columns define the formatters once %>
<% table.with_column(key: :balance, formatter: :currency) %>
<% table.with_column(key: :verified, formatter: :boolean) %>
<% table.with_column(key: :created_at, formatter: :relative_time) %>

Toolbar

Use the toolbar slot to add content above the table. Perfect for search inputs, filters, and action buttons.

How the Toolbar Works

  • Layout only: The toolbar slot provides styled placement inside the table wrapper
  • You wire the logic: Search/filter inputs must be connected to your own controller actions
  • Part of the table: When using shape: :rounded, the toolbar appears inside the rounded border (this is intentional)
  • Use Turbo: Combine with turbo_frame for live filtering without page reloads
NameEmailRole
John Doejohn@example.com Admin
Jane Smithjane@example.com Editor
Table with Toolbar
<%= rui_table do |table| %>
    <%# Toolbar slot - renders above the table %>
    <% table.with_toolbar do %>
  <div class="w-full flex items-center justify-between gap-4">
    <div class="flex-1 max-w-sm">
      <%= rui_input(name: :search, placeholder: "Search...", size: :sm, validation: false) do |input| %>
        <% input.with_prefix do %>
          <%= rui_icon(:magnifying_glass, size: :sm, standalone: false) %>
        <% end %>
      <% end %>
    </div>
    <div class="flex items-center gap-2">
      <%= rui_button("Export", variant: :outline, size: :sm) do |btn| %>
        <% btn.with_icon(:arrow_down_tray) %>
      <% end %>
      <%= rui_button("Add User", color: :primary, size: :sm) do |btn| %>
        <% btn.with_icon(:plus) %>
      <% end %>
    </div>
  </div>
    <% end %>

    <% table.with_column(key: :name, label: "Name") %>
...
    <% end %>

Toolbar with Filters

NameStatusRole
Alice Brown Active Admin
Bob Wilson Pending Editor

Making Filters Work (Turbo Integration)

To make search and filters actually work, wrap your toolbar inputs in a form and use Turbo Frames. The table component doesn't include filtering logic - you implement it in your controller.

Working Search with Turbo
# In your view:
      <%= rui_table(turbo_frame: "users-table") do |table| %>
      <% table.with_toolbar do %>
    <%= form_with url: users_path, method: :get, data: { turbo_frame: "users-table" } do |f| %>
      <div class="flex items-center gap-3">
        <%= f.text_field :q,
            value: params[:q],
            placeholder: "Search...",
            class: "your-input-classes",
            data: { action: "input->debounce#perform" } %>
        <%= f.select :status, options_for_select([["All", ""], ["Active", "active"]], params[:status]) %>
        <%= f.submit "Search" %>
      </div>
    <% end %>
      <% end %>

  <!-- columns and rows... -->
      <% end %>

# In your controller:
def index
  @users = User.all
  @users = @users.where("name ILIKE ?", "%#{params[:q]}%") if params[:q].present?
  @users = @users.where(status: params[:status]) if params[:status].present?
end

Sorting

Enable sortable columns for client-side sorting or provide a URL for server-side sorting.

NameEmailCreated
Alice Brownalice@example.comJan 15, 2024
Bob Wilsonbob@example.comFeb 22, 2024
Sortable Table
# Enable sorting with current state
      <%= rui_table(
  sortable: true,
  sort_column: "name",
  sort_direction: "asc"
) do |table| %>
      <% table.with_column(key: :name, label: "Name", sortable: true) %>
      <% table.with_column(key: :email, label: "Email", sortable: true) %>
      <% table.with_column(key: :created_at, label: "Created", sortable: true) %>
      <% end %>

# For server-side sorting, provide a sort_url:
      <%= rui_table(
  sortable: true,
  sort_column: params[:sort],
  sort_direction: params[:dir],
  sort_url: users_path
) do |table| %>
  ...
      <% end %>

Row Selection

Enable checkbox selection with a select-all header checkbox.

NameEmailStatus
John Doejohn@example.com Active
Jane Smithjane@example.com Pending
Bob Johnsonbob@example.com Inactive
Selectable Rows
<%= rui_table(selectable: true) do |table| %>
      <% table.with_column(key: :name, label: "Name") %>
      <% table.with_column(key: :email, label: "Email") %>

      <% table.with_row(id: 1) do |row| %>
    <% row.with_cell { "John Doe" } %>
    <% row.with_cell { "john@example.com" } %>
      <% end %>
      <% end %>

# JavaScript API - get selected row IDs:
# tableController.getSelectedIds() => [1, 3, 5]

Bulk Actions

Add an action bar that appears when rows are selected.

NameEmail
John Doejohn@example.com
Jane Smithjane@example.com
Bulk Actions Bar
<%= rui_table(selectable: true) do |table| %>
      <% table.with_bulk_actions do %>
    <%= rui_button("Delete Selected", color: :danger, size: :sm) %>
    <%= rui_button("Export", color: :zinc, size: :sm) %>
      <% end %>

      <% table.with_column(key: :name, label: "Name") %>
  ...
      <% end %>

# The bulk actions bar shows "X selected" count
# and is hidden when no rows are selected

Quick Setup: rui_bulk_action Helper

Use rui_bulk_action for automatic form wiring. Selected IDs are automatically submitted.

rui_bulk_action (copy-paste ready)
<%= rui_table(selectable: true) do |table| %>
    <% table.with_bulk_actions do %>
  <%= rui_bulk_action "Delete Selected",
        url: bulk_destroy_posts_path,
        method: :delete,
        confirm: "Delete selected posts?",
        color: :danger,
        size: :sm %>
  <%= rui_bulk_action "Export",
        url: export_posts_path,
        method: :post,
        color: :zinc,
        size: :sm %>
    <% end %>
...
    <% end %>
Controller - receives params[:ids]
# app/controllers/posts_controller.rb
def bulk_destroy
  ids = JSON.parse(params[:ids])
  Post.where(id: ids).destroy_all
  redirect_to posts_path, notice: "#{ids.size} posts deleted"
end

def export
  ids = JSON.parse(params[:ids])
  @posts = Post.where(id: ids)
  respond_to do |format|
    format.csv { send_data @posts.to_csv }
  end
end
Routes
# config/routes.rb
resources :posts do
  collection do
    delete :bulk_destroy
    post :export
  end
end

Options

Parameter Type Default Description
text String Button label text
url String Form action URL (required)
method Symbol :post HTTP method (:post, :patch, :put, :delete)
confirm String nil Turbo confirm dialog message
param_name Symbol :ids Name of the param containing selected IDs
color, size, variant Symbol Button defaults All rui_button options are supported

Visual Variants

Customize appearance with striped rows, borders, and hover effects.

Striped

NameEmail
User 1user1@example.com
User 2user2@example.com
User 3user3@example.com
User 4user4@example.com

Bordered

NameEmailRole
John Doejohn@example.comAdmin
Jane Smithjane@example.comEditor

Sticky Header

Keep the header visible during vertical scrolling. The component automatically creates a scrollable container with the specified max height.

The header automatically gets a solid background color to prevent content from showing through when scrolling.

CustomerDateStatusAmount
Customer 1Jan 11, 2026 Pending $366.31
Customer 2Jan 10, 2026 Pending $542.97
Customer 3Jan 09, 2026 Paid $222.73
Customer 4Jan 08, 2026 Pending $189.44
Customer 5Jan 07, 2026 Paid $97.57
Customer 6Jan 06, 2026 Refunded $476.31
Customer 7Jan 05, 2026 Refunded $468.7
Customer 8Jan 04, 2026 Refunded $501.76
Customer 9Jan 03, 2026 Paid $357.94
Customer 10Jan 02, 2026 Pending $125.2
Customer 11Jan 01, 2026 Paid $504.74
Customer 12Dec 31, 2025 Refunded $390.76
Visual Variants
# Striped rows (alternating backgrounds)
      <%= rui_table(striped: true) do |table| %>
  ...
      <% end %>

# Bordered cells
      <%= rui_table(bordered: true) do |table| %>
  ...
      <% end %>

# Sticky header with custom max height (default: 24rem)
      <%= rui_table(sticky_header: true, sticky_max_height: "16rem") do |table| %>
  ...
      <% end %>

# Disable hover effect (enabled by default)
      <%= rui_table(hoverable: false) do |table| %>
  ...
      <% end %>

Shape

Tables can have square (default) or rounded corners. Rounded tables work best with borders for a polished look.

Rounded

NameEmailRole
John Doejohn@example.comAdmin
Jane Smithjane@example.comEditor

Square (Default)

NameEmailRole
John Doejohn@example.comAdmin
Jane Smithjane@example.comEditor
Table Shape
# Rounded corners (best with bordered)
      <%= rui_table(shape: :rounded, bordered: true) do |table| %>
  ...
      <% end %>

# Square corners (default)
      <%= rui_table(shape: :square) do |table| %>
  ...
      <% end %>

Striped Columns

In addition to striped rows, you can enable striped columns for alternating column backgrounds. This is useful for wide tables to help track data across rows.

Q1 2024Q2 2024Q3 2024Q4 2024
$12,500$15,200$18,900$22,100
$8,300$9,100$11,400$13,200
$6,700$7,800$9,200$10,500

Combined with Striped Rows

ProductJanFebMar
Widget A150180210
Widget B90120145
Widget C200230280
Widget D758595
Striped Columns
# Striped columns only
      <%= rui_table(striped_columns: true) do |table| %>
  ...
      <% end %>

# Combined: striped rows + striped columns
      <%= rui_table(striped: true, striped_columns: true) do |table| %>
  ...
      <% end %>

Sizes

Five size options control cell padding and text size.

Size: xs

NameEmail
John Doejohn@example.com

Size: sm

NameEmail
John Doejohn@example.com

Size: base

NameEmail
John Doejohn@example.com

Size: lg

NameEmail
John Doejohn@example.com

Size: xl

NameEmail
John Doejohn@example.com
Table Sizes
# Extra small
      <%= rui_table(size: :xs) do |table| %>

# Small
      <%= rui_table(size: :sm) do |table| %>

# Base (default)
      <%= rui_table(size: :base) do |table| %>

# Large
      <%= rui_table(size: :lg) do |table| %>

# Extra large
      <%= rui_table(size: :xl) do |table| %>

Responsive Modes

Three modes for handling tables on mobile devices. Resize your browser or view on mobile to see the difference.

Scroll Mode (Default)

Table scrolls horizontally on small screens. Best for data-heavy tables where users need to see exact values.

IDNameEmailRoleStatus
001John Doejohn@example.comAdmin Active
002Jane Smithjane@example.comEditor Pending

Cards Mode

Each row becomes a card on mobile with label/value pairs. Best for user-facing data displays.

NameJohn Doe
Emailjohn@example.com
RoleAdmin
NameJane Smith
Emailjane@example.com
RoleEditor

Stack Mode

Cells stack vertically on mobile with labels above values. Good for forms or detail views.

John Doe
NameJohn Doe
Emailjohn@example.com
RoleAdmin
Jane Smith
NameJane Smith
Emailjane@example.com
RoleEditor
Responsive Modes
# Scroll mode (default) - horizontal scroll on mobile
      <%= rui_table(responsive_mode: :scroll) do |table| %>
  ...
      <% end %>

# Cards mode - each row becomes a card on mobile
      <%= rui_table(responsive_mode: :cards) do |table| %>
  ...
      <% end %>

# Stack mode - cells stack vertically on mobile
      <%= rui_table(responsive_mode: :stack) do |table| %>
  ...
      <% end %>

# Disable responsive behavior
      <%= rui_table(responsive: false) do |table| %>
  ...
      <% end %>

Pagination

Integrate with Pagy for pagination info display. The table shows "Showing X-Y of Z" in the footer.

See the Pagination documentation for standalone pagination with variants, sizes, colors, alignment, page jumper, and more.

Blog Posts

Showing page 2 of 6 (26 total posts)

TitleCategoryAuthorStatus
Animation Principles for UI Design Emma Wilson Published
Building Reusable Table Components Tutorials Sarah Chen Published
Rails 8 New Features Overview Engineering Ahmed Ibrahim Published
Responsive Design Patterns Design Mike Johnson Published
Modal Dialogs Done Right Tutorials Emma Wilson Published

Standalone Pagination

You can also use rui_pagination outside the table for custom layouts.

Pagy Integration
# In your controller:
def index
  @pagy, @posts = pagy(Post.all, limit: 5)
end

# In your view:
      <%= rui_table(
  pagy: @pagy,
  title: "Blog Posts",
  description: "Page #{@pagy.page} of #{@pagy.last}"
) do |table| %>
      <% table.with_column(key: :title, label: "Title") %>
      <% table.with_column(key: :category, label: "Category") %>
      <% table.with_column(key: :status, label: "Status") %>

      <% @posts.each do |post| %>
    <% table.with_row(id: post.id) do |row| %>
      <% row.with_cell { post.title } %>
      <% row.with_cell { post.category } %>
      <% row.with_cell { rui_badge(post.status, color: :success) } %>
    <% end %>
      <% end %>
      <% end %>

# Add Pagy navigation below the table
      <%== pagy_nav(@pagy) %>

# The table footer shows "Showing 1-5 of 26"

Empty State

Customize what shows when the table has no data.

Default Empty State

NameEmail

No data found

Custom Message

NameEmail

No users match your search criteria

Custom Empty State

NameEmail

No users yet

Get started by creating a new user.

Empty State Options
# Custom message
      <%= rui_table(empty_message: "No users found") do |table| %>
  ...
      <% end %>

# Custom empty state slot
      <%= rui_table do |table| %>
      <% table.with_empty_state do %>
    <div class="text-center py-8">
      <%= rui_icon(:inbox, size: :xl3) %>
      <h3 class="mt-4 font-medium">No users yet</h3>
      <%= rui_button("Add User", color: :primary) %>
    </div>
      <% end %>
  ...
      <% end %>

Loading State

Show skeleton placeholders while data is loading.

NameEmailRole
Loading State
# Show 5 skeleton rows (default)
      <%= rui_table(loading: true) do |table| %>
      <% table.with_column(key: :name, label: "Name") %>
      <% table.with_column(key: :email, label: "Email") %>
      <% end %>

# Customize number of skeleton rows
      <%= rui_table(loading: true, loading_rows: 10) do |table| %>
  ...
      <% end %>

Turbo Integration

Wrap the table in a Turbo Frame for seamless updates without full page reloads. The pagination example above demonstrates this - clicking page numbers updates only the table content.

Built-in Turbo Frame

Pass turbo_frame to automatically wrap table content:

NameRoleAction
Alice Johnson Admin Edit
Bob Smith Editor Edit
Turbo Frame Integration
# Option 1: Built-in turbo_frame param
      <%= rui_table(turbo_frame: "users_table") do |table| %>
      <% table.with_column(key: :name, label: "Name") %>
  ...
      <% end %>

# Option 2: Manual Turbo Frame wrapper (more control)
<turbo-frame id="users_table">
      <%= rui_table do |table| %>
    ...
      <% end %>

  <!-- Pagination links target the frame -->
      <%= link_to "Next", users_path(page: 2),
      data: { turbo_frame: "users_table" } %>
</turbo-frame>

# Sorting with Turbo
      <%= link_to "Sort by Name", users_path(sort: :name, dir: :asc),
    data: { turbo_frame: "users_table" } %>

Turbo Stream Updates

For real-time updates (e.g., row added/removed), use Turbo Streams:

Turbo Stream Example
# In controller action
def create
  @user = User.create!(user_params)

  respond_to do |format|
    format.turbo_stream do
      render turbo_stream: turbo_stream.prepend(
        "users_table_body",
        partial: "users/row",
        locals: { user: @user }
      )
    end
  end
end

# In view - add target to tbody
      <%= rui_table do |table| %>
  ...
  <!-- Add id to tbody for streaming -->
  <tbody id="users_table_body">
    ...
  </tbody>
      <% end %>

Real-World Examples

RapidRailsUI components are designed to work seamlessly together. Tables frequently combine with other components to create rich, interactive interfaces. This div demonstrates real-world patterns using multiple components.

Dialog + Table

Modal dialogs for detail views, edit forms, and delete confirmations.

Dropdown + Table

Action menus for row operations like edit, duplicate, delete.

Combobox + Table

Searchable selects for inline editing like assignee or category.

Date + Table

Date pickers for due dates, scheduled times, or date ranges.

User Management

A complete user management table with avatars, status badges, role indicators, and action buttons.

Team Members

Manage your team members and their account permissions.

Select all (3 items)
Tom Cook
Tom Cook
tom@example.com
Role
Admin
Status Active
JoinedJan 15, 2024
admin
Sarah Chen
Sarah Chen
sarah@example.com
Role
Editor
Status Active
JoinedFeb 22, 2024
admin
Mike Johnson
Mike Johnson
mike@example.com
Role
Viewer
Status Pending
JoinedMar 10, 2024
admin

Order History

E-commerce order table with order numbers, product details, and payment status.

OrderProductDateStatusAmount
#ORD-2024-001
Premium Subscription (Annual)
Jan 5, 2024 Completed $199.00
#ORD-2024-002
Pro License (Monthly)
Jan 12, 2024 Processing $29.00
#ORD-2024-003
Enterprise Setup
Jan 15, 2024 Refunded $499.00
Total Revenue: $228.00

Analytics Dashboard

Metrics table with trend indicators and progress visualization using striped columns.

MetricTodayThis WeekThis MonthTrend
New Users
1521,0244,392
+12%
Page Views
8,23452,891198,432
+8%
Avg. Session
4m 32s4m 18s3m 56s
-3%
Revenue
$2,340$18,290$72,450
+24%

Overflow Scrolling

Tables with many columns can use horizontal scrolling. Set responsive_mode: :scroll (the default) to enable overflow behavior.

Resize your browser window to see horizontal scrolling in action. The table container becomes scrollable when content exceeds available width.

IDFull NameEmail AddressDepartmentJob TitleOffice LocationHire DateStatusAnnual Salary
001Alexandra Thompsona.thompson@company.comEngineeringSenior DeveloperSan FranciscoMar 2021 Active $145,000
002Benjamin Rodriguezb.rodriguez@company.comMarketingMarketing ManagerNew YorkJun 2020 Active $98,000
003Catherine Williamsc.williams@company.comDesignUX LeadAustinSep 2022 On Leave $115,000
Overflow Scrolling
# Wide table with horizontal scroll (default behavior)
      <%= rui_table(responsive_mode: :scroll, bordered: true, shape: :rounded) do |table| %>
      <% table.with_column(key: :id, label: "ID", width: "60px") %>
      <% table.with_column(key: :name, label: "Full Name", width: "180px") %>
      <% table.with_column(key: :email, label: "Email Address", width: "220px") %>
      <% table.with_column(key: :department, label: "Department", width: "150px") %>
      <% table.with_column(key: :role, label: "Job Title", width: "180px") %>
      <% table.with_column(key: :location, label: "Office Location", width: "150px") %>
      <% table.with_column(key: :hire_date, label: "Hire Date", width: "120px") %>
      <% table.with_column(key: :status, label: "Status", width: "100px") %>
  ...
      <% end %>

# responsive_mode options:
# - :scroll (default) - Horizontal scroll when table is too wide
# - :cards - Transform to card layout on mobile
# - :stack - Transform to stacked layout on mobile

Table Filter

Combine tables with input fields and dropdowns to create filterable data views. This pattern uses existing RapidRailsUI components.

NameEmailRoleStatus
Alice Johnsonalice@example.comAdmin Active
Bob Smithbob@example.comEditor Active
Carol Daviscarol@example.comViewer Pending
Table with Filters
<%# Filter Bar %>
<div class="flex flex-col sm:flex-row gap-3 mb-4">
  <div class="flex-1">
    <%= rui_input(placeholder: "Search users...", prefix_icon: :search, size: :sm, validation: false) %>
  </div>
  <div class="flex gap-2">
    <%= rui_dropdown(text: "Status", size: :sm, variant: :outline, chevron: true) do |d| %>
      <% d.with_item(text: "All Statuses") %>
      <% d.with_item(text: "Active") %>
      <% d.with_item(text: "Pending") %>
    <% end %>
    <%= rui_dropdown(text: "Role", size: :sm, variant: :outline, chevron: true) do |d| %>
      <% d.with_item(text: "All Roles") %>
      <% d.with_item(text: "Admin") %>
      <% d.with_item(text: "Editor") %>
    <% end %>
  </div>
</div>

      <%# Results Table - wrap in Turbo Frame for live filtering %>
      <%= turbo_frame_tag "users_table" do %>
      <%= rui_table(striped: true, hoverable: true) do |table| %>
    ...
      <% end %>
      <% end %>

Table with Modal

Combine tables with the Dialog component for detail views, edit forms, or confirmations. Dialogs support various triggers and can contain any content including forms.

ProductSKUStockActions
Wireless Headphones Pro
WHP-00142

Product Details

Wireless Headphones Pro (WHP-001)

Price

$199.99

Stock

42 units

Category

Audio

Status In Stock

Premium wireless headphones with active noise cancellation and 30-hour battery life.

USB-C Charging Hub
UCH-0428

Product Details

USB-C Charging Hub (UCH-042)

Price

$79.99

Stock

8 units

Category

Accessories

Status Low Stock

6-port USB-C hub with 100W pass-through charging and HDMI output.

View Details Modal
<%= rui_dialog(
  trigger: rui_button("View", size: :sm, variant: :outline),
  title: "Product Details",
  description: "Wireless Headphones Pro"
) do %>
  <div class="grid grid-cols-2 gap-4">
    <div>
      <span class="text-zinc-500">Price</span>
      <p class="font-semibold">$199.99</p>
    </div>
    <div>
      <span class="text-zinc-500">Stock</span>
      <%= rui_badge("In Stock", color: :success, size: :sm) %>
    </div>
  </div>
      <% end %>

Edit Form Modal

Use dialogs to display edit forms. The form can submit via Turbo for seamless updates.

NameEmailRoleEDIT
AJ Alice Johnson
alice@example.com Admin

Edit User

Update user information

BS Bob Smith
bob@example.com Editor

Edit User

Update user information

Edit Form Modal
<%= rui_dialog(
  trigger: rui_button("Edit", size: :sm, variant: :ghost, color: :sky),
  title: "Edit User",
  description: "Update user information"
) do %>
  <div class="space-y-4">
    <%= rui_input(method: :name, label: "Name", value: user.name, size: :sm) %>
    <%= rui_input(method: :email, type: :email, label: "Email", size: :sm) %>
    <%= rui_select(method: :role, label: "Role", collection: roles, size: :sm) %>
    <div class="flex justify-end gap-2 pt-4">
      <%= rui_button("Cancel", variant: :outline, size: :sm, data: { action: "click->dialog#close" }) %>
      <%= rui_button("Save", color: :primary, size: :sm, data: { action: "dialog#submit" }) %>
    </div>
  </div>
      <% end %>

Delete Confirmation Modal

Use dialogs for destructive action confirmations. The danger color variant signals the action severity.

NameCreated
Project AlphaJan 15, 2026

Delete Project

Are you sure you want to delete Project Alpha?

Project BetaFeb 3, 2026

Delete Project

Are you sure you want to delete Project Beta?

Delete Confirmation Modal
<%= rui_dialog(
  trigger: rui_button("Delete", size: :xs, variant: :ghost, color: :danger),
  title: "Delete Project",
  description: "Are you sure you want to delete this project?"
) do %>
  <div class="space-y-4">
    <%= rui_alert(
      type: :danger,
      title: "This action cannot be undone",
      message: "All project data will be permanently deleted."
    ) %>
    <div class="flex justify-end gap-2">
      <%= rui_button("Cancel", variant: :outline, size: :sm, data: { action: "click->dialog#close" }) %>
      <%= rui_button("Delete", color: :danger, size: :sm) %>
    </div>
  </div>
      <% end %>

Table with Dropdown Actions

Use the Dropdown component for compact action menus on each row. This pattern is ideal when you have multiple actions but limited horizontal space.

OrderCustomerTotalStatus
#ORD-2024-001
SC Sarah Connor
$1,249.00 Shipped
#ORD-2024-002
JW John Wick
$89.99 Processing
#ORD-2024-003
ER Ellen Ripley
$456.50 Pending
Dropdown Actions Menu
<% row.with_cell(align: :right) do %>
      <%= rui_dropdown(icon: :more_vertical, size: :sm, variant: :ghost, width: :auto) do |d| %>
    <% d.with_item(text: "View Order", icon: :eye) %>
    <% d.with_item(text: "Edit Order", icon: :edit) %>
    <% d.with_item(text: "Download Invoice", icon: :download) %>
    <% d.with_divider %>
    <% d.with_item(text: "Refund", icon: :rotate_ccw, color: :warning) %>
    <% d.with_item(text: "Cancel Order", icon: :x, color: :danger) %>
      <% end %>
      <% end %>

# Dropdown triggers:
# - icon: :more_vertical (three dots) - most common
# - icon: :more_horizontal (three dots horizontal)
# - icon: :chevron_down (with text)
# - text: "Actions" (text-only)

Table with Combobox

Use the Combobox component for searchable inline selection. Perfect for assigning users, selecting categories, or any field with many options.

TaskPriorityAssigneeStatus
Implement user authentication
High
In Progress
Design landing page mockups
Medium
Todo
Write API documentation
Low
Todo
Combobox for Assignee Selection
<% row.with_cell do %>
      <%= rui_combobox(
    collection: [
      ["Alice Johnson", "alice"],
      ["Bob Smith", "bob"],
      ["Carol Davis", "carol"]
    ],
    selected: "alice",
    placeholder: "Assign...",
    size: :sm,
    searchable: true
  ) %>
      <% end %>

# Collection format: ["Label", "value"]
# Optional third element: icon symbol like :user
# Use size: :sm for compact inline display

Table with Date Picker

Use the Date component for due dates, scheduling, or any date-related fields. The date picker integrates seamlessly within table cells.

ProjectMilestoneDue DateStatus
Website Redesign
Phase 1 - Discovery
On Track
Mobile App v2
Beta Release
At Risk
API Migration
Database Cutover
Overdue
Date Picker in Table
<% row.with_cell do %>
      <%= rui_date(
    value: project.due_date,
    size: :sm,
    min: Date.today  # Optional: prevent past dates
  ) %>
      <% end %>

# The date component automatically formats dates
# and opens a calendar/date picker on click
# Use size: :sm for compact inline display

Headless Table

Set headless: true to hide the table header. Useful for simple data displays or when labels are self-explanatory.

Meeting with Design Team
Today, 2:00 PM Upcoming
Quarterly Report Due
Tomorrow, 5:00 PM Pending
Code Review Completed
Yesterday Done
Headless Table
# Table without header row
      <%= rui_table(headless: true, bordered: true, shape: :rounded) do |table| %>
      <% table.with_row(id: 1) do |row| %>
    <% row.with_cell do %>
      <div class="flex items-center gap-3">
        <%= rui_icon(:calendar, size: :sm, color: :primary) %>
        <span class="font-medium">Meeting with Design Team</span>
      </div>
    <% end %>
    <% row.with_cell { "Today, 2:00 PM" } %>
    <% row.with_cell(align: :right) { rui_badge("Upcoming", color: :info) } %>
      <% end %>
  ...
      <% end %>

# Note: When headless: true, column definitions are optional
# but can still be used for mobile responsive views

Custom Styling

Override or extend styles using the class: parameter. RapidRailsUI uses tailwind_merge to intelligently merge custom classes.

Available Class Overrides

  • class: - Applied to the outer wrapper element
  • Any HTML attribute via **options (id, data-*, aria-*, etc.)

Custom Wrapper Styling

NameStatus
Custom styled table Active
With shadow and ring Pro
Custom Wrapper Classes
<%# Add shadow and ring to the wrapper %>
      <%= rui_table(
  class: "shadow-lg ring-1 ring-zinc-900/5",
  bordered: true,
  shape: :rounded
) do |table| %>
  ...
      <% end %>

      <%# Add custom data attributes %>
      <%= rui_table(
  id: "my-table",
  data: { controller: "my-custom-table" }
) do |table| %>
  ...
      <% end %>

Accessibility

The Table component follows WAI-ARIA best practices for accessible data tables.

ARIA Attributes

  • role="grid" on the table element for screen readers
  • scope="col" on header cells for column association
  • aria-sort indicates current sort direction (ascending/descending/none)
  • aria-label on checkboxes ("Select all rows", "Select row")
  • aria-describedby links table to caption for context

Keyboard Navigation

  • Tab navigates between interactive elements (sort headers, checkboxes, links)
  • Enter or Space activates sort headers and checkboxes
  • Focus states are clearly visible with ring styles for all interactive elements

Semantic HTML

  • Uses native <table>, <thead>, <tbody>, <tfoot> elements
  • Header cells use <th> with scope, data cells use <td>
  • Sortable headers use <button> elements for proper interaction
  • Caption slot renders as screen-reader-only .sr-only element

Accessible Table Example

Full Accessibility Setup
<%= rui_table(
  sortable: true,
  selectable: true,
  sort_column: "name",
  sort_direction: "asc"
) do |table| %>
  <!-- Screen-reader only caption -->
      <% table.with_caption { "List of team members with names, roles, and contact information" } %>

      <% table.with_column(key: :name, label: "Name", sortable: true) %>
      <% table.with_column(key: :role, label: "Role") %>
      <% table.with_column(key: :email, label: "Email") %>

      <% @users.each do |user| %>
    <% table.with_row(id: user.id) do |row| %>
      <% row.with_cell { user.name } %>
      <% row.with_cell { user.role } %>
      <% row.with_cell { user.email } %>
    <% end %>
      <% end %>
      <% end %>

# Generated HTML includes:
# <table role="grid" aria-describedby="table-123-caption">
#   <div id="table-123-caption" class="sr-only">...</div>
#   <thead>
#     <tr>
#       <th scope="col">
#         <input type="checkbox" aria-label="Select all rows">
#       </th>
#       <th scope="col" aria-sort="ascending">
#         <button>Name <svg>...</svg></button>
#       </th>
#       ...
#     </tr>
#   </thead>
# </table>

JavaScript API

The Table component uses a Stimulus controller with these methods and targets.

Method/Target Description
sort(event) Trigger sort on a column (client-side or navigates to sort_url)
toggleAll(event) Toggle all row checkboxes via header checkbox
toggleRow(event) Toggle individual row checkbox
getSelectedIds() Returns array of selected row IDs
selectAllTarget Header "select all" checkbox
rowCheckboxTargets All row checkboxes
bulkActionsTarget Bulk actions bar container
selectedCountTarget Selected count display element
JavaScript Integration
# Get selected row IDs in a Stimulus controller
export default class extends Controller {
  static outlets = ["table"]

  deleteSelected() {
    const ids = this.tableOutlet.getSelectedIds()
    // ids = [1, 3, 5]
    fetch("/users/bulk_delete", {
      method: "DELETE",
      body: JSON.stringify({ ids })
    })
  }
}

# Or use data-action directly:
<button data-action="my-controller#deleteSelected">
  Delete Selected
</button>

API Reference

rui_table

Full-featured data table with sorting, selection, pagination, and responsive modes

Parameter Type Default Description
id String auto-generated Unique ID for the table
empty_message String "No data found" Message when table has no rows

Caption

Visible caption options

Parameter Type Default Description
title String Visible title above the table
description String Visible description below the title

Appearance

Visual styling options

Parameter Type Default Description
size Symbol :base Cell padding and text size
:xs :sm :base :lg :xl
shape Symbol :rounded Table corner shape
:square :rounded
striped Boolean false Alternate row backgrounds
striped_columns Boolean false Alternate column backgrounds
hoverable Boolean true Highlight rows on hover
bordered Boolean true Add outer border and vertical borders between cells
headless Boolean false Hide the table header (thead)
sticky_header Boolean false Fix header when scrolling
sticky_max_height String "24rem" Max height for scrollable area (when sticky_header is true)

Sorting

Column sorting configuration

Parameter Type Default Description
sortable Boolean false Enable sorting on columns
sort_column String Currently sorted column key
sort_direction String Current sort direction: asc or desc
sort_url String URL for server-side sorting

Selection

Row selection options

Parameter Type Default Description
selectable Boolean false Show row checkboxes

Responsive

Mobile layout options

Parameter Type Default Description
responsive Boolean true Enable responsive behavior
responsive_mode Symbol :scroll Mobile layout mode
:scroll :cards :stack

Pagination

Pagy integration

Parameter Type Default Description
pagy Pagy Pagy instance for pagination info

Turbo

Turbo Frame integration

Parameter Type Default Description
turbo_frame String Wrap table in Turbo Frame with this ID

Loading

Loading state options

Parameter Type Default Description
loading Boolean false Show skeleton loading state
loading_rows Integer 5 Number of skeleton rows to show

Column Formatters

Built-in formatters for common data types

Parameter Type Default Description
:date Symbol Format as date (e.g., Jan 15, 2024)
:datetime Symbol Format as date and time
:time Symbol Format as time only
:relative_time Symbol Time ago in words (e.g., 2 days ago)
:currency Symbol Format as currency ($1,234.56)
:number Symbol Format with thousands separator
:percentage Symbol Format as percentage (75.5%)
:boolean Symbol Format as Yes/No or custom text
:truncate Symbol Truncate long text with ellipsis
:badge Symbol Render as colored badge component

Custom Styling

Override default styles

Parameter Type Default Description
class String Custom CSS classes for the wrapper element (merged with defaults)
id String auto-generated Custom ID for the wrapper element
data Hash Custom data attributes (data-controller, etc.)

Slots

Content slots for customizing component parts

Slot Description
column Define table columns: table.with_column(key: :name, label: 'Name', sortable: true, align: :left, width: '200px', formatter: :date)
row Add data rows: table.with_row(id: 1, data: user, color: :danger) { |row| row.with_cell { 'value' } }
toolbar Content above the table: table.with_toolbar { search_field + filter_button }
empty_state Custom empty state content: table.with_empty_state { 'Custom message' }
bulk_actions Bulk action buttons: table.with_bulk_actions { button_tag 'Delete Selected' }
footer Table footer row: table.with_footer { 'Total: $1,000' }
caption Accessible table caption: table.with_caption { 'List of users' }

Related Components