Skip to main content
Admin Tables are part of the upcoming Spree 5.3 release slated for January 2026.
Spree Admin provides a flexible table system for displaying resource listings with customizable columns, sorting, filtering, and bulk actions. The Tables DSL allows you to define table configurations for your resources and extend existing ones.
The Tables system uses a declarative API accessible via Spree.admin.tables. This allows you to programmatically add, modify, and remove table columns and bulk actions directly in your initializers.

Basic Usage

Creating a New Table

Register a new table for your resource in config/initializers/spree.rb:
config/initializers/spree.rb
Rails.application.config.after_initialize do
  # Register a new table
  Spree.admin.tables.register(:brands, model_class: Spree::Brand, search_param: :name_cont)

  # Add columns
  Spree.admin.tables.brands.add :name,
    label: :name,
    type: :link,
    sortable: true,
    default: true,
    position: 10

  Spree.admin.tables.brands.add :products_count,
    label: :products,
    type: :number,
    sortable: false,
    default: true,
    position: 20,
    method: ->(brand) { brand.products.count }

  Spree.admin.tables.brands.add :created_at,
    label: :created_at,
    type: :datetime,
    sortable: true,
    default: false,
    position: 30
end

Using the Table in Views

Render the table in your index view using the render_table helper:
app/views/spree/admin/brands/index.html.erb
<%= render_table @collection, :brands %>
With additional options:
<%= render_table @collection, :brands,
                 bulk_operations: true,
                 export_type: Spree::Exports::Brands %>

Table Registration Options

When registering a table, you can specify these options:
model_class
Class
required
The model class for the table (e.g., Spree::Brand).
search_param
Symbol
default:":name_cont"
The Ransack search parameter for the search box.
search_placeholder
String
Custom placeholder text for the search box.
row_actions
Boolean
default:"false"
Enable row action buttons (edit/delete dropdown).
row_actions_edit
Boolean
default:"true"
Show edit action in row actions dropdown.
row_actions_delete
Boolean
default:"false"
Show delete action in row actions dropdown.
new_resource
Boolean
default:"true"
Show “New Resource” button when collection is empty.
date_range_param
Symbol
Enable date range filter for the specified column (e.g., :created_at).
Action for link columns (:edit or :show).

Column Options

All columns support the following options:
label
Symbol or String
required
The column header label. Can be a symbol (translation key using Spree.t) or a string.
type
String
default:"string"
The column type. Determines how the value is rendered.Available types:
  • string - Plain text
  • number - Numeric value
  • date - Date formatted with spree_date helper
  • datetime - Relative time with spree_time_ago helper
  • money - Currency formatted with Spree::Money
  • status - Badge with status-based styling
  • link - Clickable link to resource
  • boolean - Active/inactive badge
  • image - Thumbnail image
  • association - Associated record name(s)
  • custom - Custom partial rendering
sortable
Boolean
default:"true"
Whether the column can be sorted.
filterable
Boolean
default:"true"
Whether the column appears in the query builder filter options.
displayable
Boolean
default:"true"
Whether the column can be shown/hidden by users. Set to false for filter-only columns.
default
Boolean
default:"false"
Whether the column is visible by default.
position
Integer
default:"999"
Column order. Lower numbers appear first.
method
Symbol or Lambda
Custom method to extract the column value. Can be a method name or lambda.
align
String
default:"left"
Text alignment: left, center, or right.
width
String
Column width class (e.g., "20" for w-20).
if
Lambda
Conditional visibility. Column only appears if lambda returns true.

Filter-specific Options

filter_type
String
Override the filter input type. Available: string, number, date, datetime, money, status, boolean, autocomplete, select.
ransack_attribute
String
Custom Ransack attribute name for filtering/sorting (defaults to column key).
operators
Array
Available filter operators. Defaults based on column type.
value_options
Array or Lambda
Options for select/status filters. Array of hashes with value and label keys.
search_url
String
URL for autocomplete filter type.

Custom Sort Options

sort_scope_asc
Symbol
Custom scope name for ascending sort (bypasses Ransack).
sort_scope_desc
Symbol
Custom scope name for descending sort (bypasses Ransack).

Custom Partial Options

partial
String
Partial path for custom type columns.
partial_locals
Hash or Lambda
Additional locals to pass to the partial. Lambda receives the record.

Column Types Examples

String Column

Spree.admin.tables.brands.add :name,
  label: :name,
  type: :string,
  sortable: true,
  default: true
Links to the resource edit or show page:
Spree.admin.tables.brands.add :name,
  label: :name,
  type: :link,
  sortable: true,
  default: true

Money Column

Displays formatted currency:
Spree.admin.tables.products.add :price,
  label: :price,
  type: :money,
  sortable: true,
  default: true,
  align: :right,
  method: ->(product) { product.display_price }

Status Column

Displays a colored badge based on the status value:
Spree.admin.tables.orders.add :state,
  label: :state,
  type: :status,
  filter_type: :select,
  sortable: true,
  default: true,
  value_options: -> {
    Spree::Order.state_machine(:state).states.map { |s|
      { value: s.name.to_s, label: s.name.to_s.humanize }
    }
  }
Status values are automatically styled:
  • Green (badge-active): active, complete, completed, paid, shipped, available
  • Yellow (badge-warning): draft, pending, processing, ready
  • Gray (badge-inactive): archived, canceled, cancelled, failed, void, inactive

Boolean Column

Spree.admin.tables.products.add :available,
  label: :available,
  type: :boolean,
  sortable: true,
  default: true

DateTime Column

Displays relative time (e.g., “2 hours ago”):
Spree.admin.tables.orders.add :completed_at,
  label: :completed_at,
  type: :datetime,
  sortable: true,
  default: true

Association Column

For displaying related records:
Spree.admin.tables.products.add :taxons,
  label: :taxons,
  type: :association,
  filter_type: :autocomplete,
  sortable: false,
  filterable: true,
  default: false,
  ransack_attribute: 'taxons_id',
  operators: [:in],
  search_url: '/admin/taxons/select_options.json',
  method: ->(product) { product.taxons.map(&:pretty_name).join(', ') }

Custom Partial Column

For complex rendering:
Spree.admin.tables.products.add :name,
  label: :name,
  type: :custom,
  sortable: true,
  default: true,
  partial: 'spree/admin/tables/columns/product_name'

# With dynamic locals
Spree.admin.tables.orders.add :customer,
  label: :customer,
  type: :custom,
  default: true,
  partial: 'spree/admin/orders/customer_summary',
  partial_locals: ->(record) { { order: record } }
The partial receives record, column, and value locals plus any custom locals.

Filter-only Column

Columns that can be filtered but not displayed:
Spree.admin.tables.orders.add :sku,
  label: :sku,
  type: :string,
  sortable: false,
  filterable: true,
  displayable: false,
  ransack_attribute: 'line_items_variant_sku'

Modifying Existing Tables

Adding Columns to Existing Tables

config/initializers/spree.rb
Rails.application.config.after_initialize do
  # Add a custom column to products
  Spree.admin.tables.products.add :vendor,
    label: 'Vendor',
    type: :string,
    sortable: false,
    default: true,
    position: 25,
    method: ->(product) { product.vendor&.name },
    if: -> { defined?(Spree::Vendor) }
end

Updating Existing Columns

Rails.application.config.after_initialize do
  # Change the position of an existing column
  Spree.admin.tables.products.update :price, position: 15

  # Make a column default
  Spree.admin.tables.products.update :sku, default: true

  # Change column label
  Spree.admin.tables.orders.update :number, label: 'Order #'
end

Removing Columns

Rails.application.config.after_initialize do
  # Remove a column entirely
  Spree.admin.tables.products.remove :sku
end

Inserting Columns at Specific Positions

Rails.application.config.after_initialize do
  # Insert before an existing column
  Spree.admin.tables.products.insert_before :price, :cost_price,
    label: 'Cost Price',
    type: :money,
    default: false

  # Insert after an existing column
  Spree.admin.tables.products.insert_after :name, :brand,
    label: 'Brand',
    type: :string,
    default: true,
    method: ->(product) { product.brand&.name }
end

Bulk Actions

Add bulk actions that appear when users select multiple rows:
Rails.application.config.after_initialize do
  Spree.admin.tables.products.add_bulk_action :set_active,
    label: 'admin.bulk_ops.products.title.set_status',
    label_options: { status: :active },
    icon: 'circle-check',
    modal_path: '/admin/products/bulk_modal?kind=set_status&status=active',
    action_path: '/admin/products/bulk_status_update?status=active',
    position: 10,
    if: -> { can?(:activate, Spree::Product) }
end

Bulk Action Options

label
String
required
Translation key or text for the action button.
label_options
Hash
Interpolation options for the label translation.
icon
String
Icon name from Tabler Icons.
modal_path
String
required
URL for the confirmation modal.
action_path
String
required
URL for the bulk action endpoint.
method
Symbol
default:":put"
HTTP method for the action (:get, :post, :put, :patch, :delete).
position
Integer
default:"999"
Order in the bulk actions menu.
if
Lambda
Conditional visibility based on user permissions.
confirm
String
Confirmation message before executing the action.

Managing Bulk Actions

# Update a bulk action
Spree.admin.tables.products.update_bulk_action :set_active, position: 5

# Remove a bulk action
Spree.admin.tables.products.remove_bulk_action :set_archived

Custom Sorting

For columns that need custom database queries:
# In your model
class Spree::Product < Spree.base_class
  scope :ascend_by_price, -> { joins(:master).order('spree_variants.price ASC') }
  scope :descend_by_price, -> { joins(:master).order('spree_variants.price DESC') }
end

# In your initializer
Spree.admin.tables.products.add :price,
  label: :price,
  type: :money,
  sortable: true,
  default: true,
  sort_scope_asc: :ascend_by_price,
  sort_scope_desc: :descend_by_price,
  method: ->(product) { product.price }

Available Tables

Spree registers tables for all built-in resources:
Table KeyModel Class
:productsSpree::Product
:ordersSpree::Order
:checkoutsSpree::Order (draft)
:usersSpree.user_class
:promotionsSpree::Promotion
:customer_returnsSpree::CustomerReturn
:option_typesSpree::OptionType
:newsletter_subscribersSpree::NewsletterSubscriber
:policiesSpree::Policy
:stock_transfersSpree::StockTransfer
:metafield_definitionsSpree::MetafieldDefinition
:gift_cardsSpree::GiftCard
:stock_itemsSpree::StockItem
:postsSpree::Post
:post_categoriesSpree::PostCategory
:webhook_endpointsSpree::WebhookEndpoint
:webhook_deliveriesSpree::WebhookDelivery

API Reference

Table Methods

table = Spree.admin.tables.products

# Column management
table.add(key, **options)           # Add a new column
table.remove(key)                   # Remove a column
table.update(key, **options)        # Update column options
table.find(key)                     # Find a column by key
table.exists?(key)                  # Check if column exists
table.insert_before(target, key, **options)
table.insert_after(target, key, **options)

# Query columns
table.available_columns             # All displayable columns
table.default_columns               # Columns shown by default
table.visible_columns(selected, ctx) # Columns for current view
table.sortable_columns              # Columns that can be sorted
table.filterable_columns            # Columns for query builder

# Bulk actions
table.add_bulk_action(key, **options)
table.remove_bulk_action(key)
table.update_bulk_action(key, **options)
table.find_bulk_action(key)
table.visible_bulk_actions(context)
table.bulk_operations_enabled?

Registry Methods

# Check if a table is registered
Spree.admin.tables.registered?(:brands)

# Get a table
Spree.admin.tables.get(:products)

# Shorthand access
Spree.admin.tables.products