docs/developer/admin/components.mdx
Spree Admin provides a set of reusable UI components that you can use in your custom admin views. These components are implemented as view helpers and integrate with Stimulus controllers for interactivity.
The dropdown component creates accessible dropdown menus with automatic positioning using Floating UI.
<%= dropdown do %>
<%= dropdown_toggle class: 'btn-light btn-sm' do %>
<%= icon('dots-vertical', class: 'mr-0') %>
<% end %>
<%= dropdown_menu do %>
<%= link_to_with_icon 'pencil', Spree.t(:edit), edit_path, class: 'dropdown-item' %>
<%= link_to_with_icon 'trash', Spree.t(:delete), delete_path,
class: 'dropdown-item text-danger',
data: { turbo_method: :delete, turbo_confirm: Spree.t(:are_you_sure) } %>
<% end %>
<% end %>
dropdownCreates the dropdown container with Stimulus controller.
<%= dropdown do %>
<!-- toggle and menu -->
<% end %>
<%= dropdown placement: 'top-end' do %>
<!-- menu opens above, aligned to end -->
<% end %>
| Option | Type | Default | Description |
|---|---|---|---|
class | String | - | Additional CSS classes |
placement | String | bottom-start | Menu placement: bottom-start, bottom-end, top-start, top-end |
direction | String | - | Legacy option: left → bottom-end, top → top-start, top-left → top-end |
portal | Boolean | - | Whether to portal the dropdown to body |
data | Hash | - | Additional data attributes |
dropdown_toggleCreates the button that toggles the dropdown menu.
<%= dropdown_toggle class: 'btn-primary' do %>
Actions <%= icon('chevron-down', class: 'ml-2') %>
<% end %>
| Option | Type | Default | Description |
|---|---|---|---|
class | String | - | Additional CSS classes (added to btn base class) |
data | Hash | - | Additional data attributes |
dropdown_menuCreates the menu container for dropdown items.
<%= dropdown_menu do %>
<%= link_to 'Option 1', '#', class: 'dropdown-item' %>
<div class="dropdown-divider"></div>
<%= link_to 'Option 2', '#', class: 'dropdown-item text-danger' %>
<% end %>
| Option | Type | Default | Description |
|---|---|---|---|
class | String | - | Additional CSS classes |
data | Hash | - | Additional data attributes |
<%= content_for :page_actions do %>
<%= dropdown do %>
<%= dropdown_toggle class: 'btn-primary' do %>
<%= icon('settings', class: 'mr-2') %>
Actions
<%= icon('chevron-down', class: 'ml-2') %>
<% end %>
<%= dropdown_menu do %>
<%= link_to_with_icon 'download', 'Export CSV', export_path(format: :csv), class: 'dropdown-item' %>
<%= link_to_with_icon 'file-export', 'Export Excel', export_path(format: :xlsx), class: 'dropdown-item' %>
<div class="dropdown-divider"></div>
<%= link_to_with_icon 'upload', 'Import', import_path, class: 'dropdown-item' %>
<div class="dropdown-divider"></div>
<%= link_to_with_icon 'trash', 'Delete All', bulk_delete_path,
class: 'dropdown-item text-danger',
data: { turbo_method: :delete, turbo_confirm: Spree.t(:are_you_sure) } %>
<% end %>
<% end %>
<% end %>
Dialogs (modals) are used for focused interactions that require user attention. They overlay the page content and must be dismissed before continuing.
<div class="dialog" data-controller="dialog">
<%= dialog_header('Edit Product') %>
<div class="dialog-body">
<!-- Dialog content -->
</div>
<div class="dialog-footer">
<%= dialog_discard_button %>
<%= turbo_save_button_tag %>
</div>
</div>
dialog_headerCreates a dialog header with title and close button.
<%= dialog_header('Confirm Action') %>
<%= dialog_header('Custom Dialog', 'my-dialog') %>
| Parameter | Type | Default | Description |
|---|---|---|---|
title | String | required | The dialog title |
controller_name | String | dialog | Stimulus controller name for the close action |
Renders:
<div class="dialog-header">
<h5 class="dialog-title">Confirm Action</h5>
<button type="button" class="btn-close" data-action="dialog#close" data-dismiss="dialog"></button>
</div>
dialog_close_buttonCreates a standalone close button for dialogs.
<%= dialog_close_button %>
<%= dialog_close_button('custom-controller') %>
| Parameter | Type | Default | Description |
|---|---|---|---|
controller_name | String | dialog | Stimulus controller name |
dialog_discard_buttonCreates a "Discard" button that closes the dialog.
<%= dialog_discard_button %>
| Parameter | Type | Default | Description |
|---|---|---|---|
controller_name | String | dialog | Stimulus controller name |
Renders:
<button type="button" class="btn btn-light" data-action="dialog#close" data-dismiss="dialog">
Discard
</button>
<%= turbo_frame_tag 'main-dialog' do %>
<div class="dialog-backdrop" data-controller="dialog" data-action="click->dialog#backdropClose keydown.esc@window->dialog#close">
<div class="dialog">
<%= dialog_header('Add New Item') %>
<%= form_with model: @item, url: items_path, data: { turbo_frame: '_top' } do |f| %>
<div class="dialog-body">
<%= f.spree_text_field :name, required: true %>
<%= f.spree_text_area :description %>
</div>
<div class="dialog-footer">
<%= dialog_discard_button %>
<%= turbo_save_button_tag 'Create Item' %>
</div>
<% end %>
</div>
</div>
<% end %>
Drawers are slide-out panels typically used for filters, secondary forms, or detailed views without leaving the current page.
<div class="drawer" data-controller="drawer">
<%= drawer_header('Filter Options') %>
<div class="drawer-body">
<!-- Drawer content -->
</div>
<div class="drawer-footer">
<%= drawer_discard_button %>
<button type="submit" class="btn btn-primary">Apply Filters</button>
</div>
</div>
drawer_headerCreates a drawer header with title and close button.
<%= drawer_header('Filters') %>
<%= drawer_header('Details', 'custom-drawer') %>
| Parameter | Type | Default | Description |
|---|---|---|---|
title | String | required | The drawer title |
controller_name | String | drawer | Stimulus controller name for the close action |
Renders:
<div class="drawer-header">
<h5 class="drawer-title">Filters</h5>
<button type="button" class="btn-close" data-action="drawer#close" data-dismiss="drawer"></button>
</div>
drawer_close_buttonCreates a standalone close button for drawers.
<%= drawer_close_button %>
<%= drawer_close_button('custom-drawer') %>
| Parameter | Type | Default | Description |
|---|---|---|---|
controller_name | String | drawer | Stimulus controller name |
drawer_discard_buttonCreates a "Discard" button that closes the drawer.
<%= drawer_discard_button %>
| Parameter | Type | Default | Description |
|---|---|---|---|
controller_name | String | drawer | Stimulus controller name |
Icons are rendered using the Tabler Icons library.
<%= icon('package') %>
<%= icon('shopping-cart', class: 'text-primary') %>
<%= icon('check', class: 'text-success', height: 24) %>
| Option | Type | Default | Description |
|---|---|---|---|
class | String | - | Additional CSS classes |
height | Integer | - | Icon size in pixels |
style | String | - | Additional inline styles |
<i class="ti ti-package"></i>
<i class="ti ti-shopping-cart text-primary"></i>
<i class="ti ti-check text-success" style="font-size: 24px !important;"></i>
For backwards compatibility, legacy icon names are automatically translated:
| Legacy Name | Tabler Icon |
|---|---|
save | device-floppy |
edit | pencil |
delete | trash |
add | plus |
cancel | x |
| Icon | Name | Usage |
|---|---|---|
| <i class="ti ti-pencil"></i> | pencil | Edit actions |
| <i class="ti ti-trash"></i> | trash | Delete actions |
| <i class="ti ti-plus"></i> | plus | Add/create actions |
| <i class="ti ti-eye"></i> | eye | View/preview |
| <i class="ti ti-download"></i> | download | Download/export |
| <i class="ti ti-upload"></i> | upload | Upload/import |
| <i class="ti ti-check"></i> | check | Success/confirm |
| <i class="ti ti-x"></i> | x | Close/cancel |
| <i class="ti ti-dots-vertical"></i> | dots-vertical | More options |
| <i class="ti ti-chevron-down"></i> | chevron-down | Expand |
| <i class="ti ti-chevron-right"></i> | chevron-right | Navigate |
Displays optimized images with automatic WebP conversion and retina support.
<%= spree_image_tag(product.images.first, width: 100, height: 100) %>
<%= spree_image_tag(current_store.logo, width: 200, height: 60) %>
<%= spree_image_tag(taxon.image, width: 400, height: 300, class: 'rounded') %>
| Option | Type | Default | Description |
|---|---|---|---|
image | Asset/Attachment | required | Spree::Asset or ActiveStorage attachment |
width | Integer | - | Display width in pixels |
height | Integer | - | Display height in pixels |
class | String | - | CSS classes |
alt | String | - | Alt text for accessibility |
loading | Symbol | - | :lazy or :eager |
<%# Product thumbnail in index table %>
<% if product.has_images? %>
<%= spree_image_tag product.default_image, width: 48, height: 48, class: 'rounded' %>
<% else %>
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 48px; height: 48px;">
<%= icon('photo-off', class: 'text-muted') %>
</div>
<% end %>
<%# Store logo in header %>
<%= spree_image_tag current_store.logo, width: 120, height: 40, alt: current_store.name %>
<%# Category image %>
<%= spree_image_tag taxon.image, width: 300, height: 200, class: 'card-img-top' %>
<% if @brand.logo.attached? %>
<%= spree_image_tag @brand.logo, width: 100, height: 100 %>
<% else %>
<div class="placeholder-image">
<%= icon('photo') %>
</div>
<% end %>
Tooltips provide additional context on hover.
<span data-controller="tooltip">
<%= icon('info-circle') %>
<%= tooltip('This is helpful information') %>
</span>
tooltipCreates a tooltip container.
<%= tooltip('Simple text tooltip') %>
<%= tooltip do %>
<strong>Rich content</strong> with <em>formatting</em>
<% end %>
| Parameter | Type | Description |
|---|---|---|
text | String | Tooltip text (or use block for rich content) |
help_bubbleCreates an info icon with a tooltip - commonly used for form field hints.
<%= help_bubble('This field is used for SEO optimization') %>
<%= help_bubble('Shown below the field', 'bottom') %>
<%= help_bubble('Custom styled', 'top', css: 'text-primary') %>
| Parameter | Type | Default | Description |
|---|---|---|---|
text | String | required | Tooltip text |
placement | String | top | Tooltip position: top, bottom, left, right |
css | String | text-xs text-muted... | CSS classes for the icon |
Output:
<span data-controller="tooltip" data-tooltip-placement-value="top">
<i class="ti ti-info-square-rounded text-xs text-muted cursor-default opacity-75"></i>
<span role="tooltip" data-tooltip-target="tooltip" class="tooltip-container">
This field is used for SEO optimization
</span>
</span>
Displays a status badge indicating active/inactive state.
<%= active_badge(product.active?) %>
<%= active_badge(user.confirmed?) %>
<%= active_badge(order.paid?, label: 'Paid') %>
| Option | Type | Default | Description |
|---|---|---|---|
condition | Boolean | required | The condition to evaluate |
label | String | Yes/No | Custom label text |
When condition is true:
<span class="badge badge-active">
<i class="ti ti-check"></i> Yes
</span>
When condition is false:
<span class="badge badge-inactive">No</span>
<%= active_badge(subscription.active?, label: subscription.active? ? 'Active' : 'Expired') %>
<%= active_badge(feature.enabled?, label: feature.enabled? ? 'Enabled' : 'Disabled') %>
Renders a user avatar with automatic fallback to initials.
<%= render_avatar(current_user) %>
<%= render_avatar(user, width: 48, height: 48) %>
<%= render_avatar(admin, class: 'avatar-lg') %>
| Option | Type | Default | Description |
|---|---|---|---|
user | Object | required | User object (must respond to avatar and name) |
width | Integer | 128 | Avatar width in pixels |
height | Integer | 128 | Avatar height in pixels |
class | String | avatar | CSS classes |
<!-- With avatar image -->
<!-- Without avatar (fallback) -->
<div class="avatar" style="width: 128px; height: 128px;">JD</div>
Copy-to-clipboard functionality with visual feedback.
<%= clipboard_component(product.sku) %>
<%= clipboard_component(api_key) %>
clipboard_componentCreates a complete clipboard component with hidden input and copy button.
<%= clipboard_component('ABC-123-XYZ') %>
| Parameter | Type | Description |
|---|---|---|
text | String | The text to copy |
Output:
<span data-controller="clipboard" data-clipboard-success-content-value="<i class='ti ti-check mr-0 font-size-sm'></i>">
<input type="hidden" name="clipboard_source" value="ABC-123-XYZ" data-clipboard-target="source">
<button type="button" class="btn btn-clipboard" data-action="clipboard#copy" data-clipboard-target="button">
<i class="ti ti-copy mr-0 font-size-sm"></i>
<span role="tooltip" data-tooltip-target="tooltip" class="tooltip-container">Copy to clipboard</span>
</button>
</span>
clipboard_buttonCreates just the copy button (for custom layouts).
<div data-controller="clipboard">
<input type="text" data-clipboard-target="source" value="custom-value">
<%= clipboard_button %>
</div>
<div class="d-flex align-items-center gap-2">
<code><%= product.sku %></code>
<%= clipboard_component(product.sku) %>
</div>
Displays a progress bar with customizable range.
<%= progress_bar_component(75) %>
<%= progress_bar_component(150, max: 200) %>
<%= progress_bar_component(50, min: 0, max: 100) %>
| Option | Type | Default | Description |
|---|---|---|---|
value | Integer | required | Current progress value |
min | Integer | 0 | Minimum value |
max | Integer | 100 | Maximum value |
<div class="progress">
<div class="progress-bar"
role="progressbar"
style="width: 75%"
aria-valuenow="75"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
<!-- Inventory level -->
<%= progress_bar_component(stock_item.count_on_hand, max: stock_item.backorderable_threshold || 100) %>
<!-- Order fulfillment -->
<%= progress_bar_component(order.shipments.shipped.count, max: order.shipments.count) %>
<!-- Upload progress -->
<%= progress_bar_component(uploaded_count, max: total_count) %>
Helpers for displaying dates and times in the user's local timezone.
spree_dateRenders a date in the user's local format.
<%= spree_date(order.created_at) %>
<%= spree_date(product.available_on) %>
spree_timeRenders a date and time in the user's local format.
<%= spree_time(order.completed_at) %>
<%= spree_time(shipment.shipped_at) %>
spree_time_agoRenders a relative time (e.g., "2 hours ago") with a tooltip showing the full timestamp.
<%= spree_time_ago(order.completed_at) %>
<%= spree_time_ago(comment.created_at) %>
Output:
<span data-controller="tooltip">
<time datetime="2024-01-15T10:30:00Z" data-local="time-ago">2 hours ago</time>
<span role="tooltip" data-tooltip-target="tooltip" class="tooltip-container">
January 15, 2024 10:30 AM
</span>
</span>
local_timeThe underlying helper from local_time gem - displays time in user's browser timezone.
<%= local_time(order.completed_at) %>
<%= local_time(event.starts_at, format: '%B %e, %Y at %l:%M %p') %>
| Helper | Output | Use Case |
|---|---|---|
spree_date | "Jan 15, 2024" | Date-only display |
spree_time | "Jan 15, 2024 10:30 AM" | Full timestamp |
spree_time_ago | "2 hours ago" | Relative time with tooltip |
local_time | "January 15, 2024 10:30 AM" | Customizable format |