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.
Dropdown
The dropdown component creates accessible dropdown menus with automatic positioning using Floating UI.
Basic Usage
<%= 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 %>
Features
- Automatic positioning - Uses Floating UI to position the menu optimally
- Auto-flip - Menu flips to stay within viewport
- Click outside to close - Menu closes when clicking outside
- Escape to close - Menu closes when pressing Escape key
- Keyboard navigation - Full keyboard accessibility
dropdown
Creates 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_toggle
Creates 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 |
Creates 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 |
Complete Example
<%= 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 %>
Dialog
Dialogs (modals) are used for focused interactions that require user attention. They overlay the page content and must be dismissed before continuing.
Basic Usage
<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>
Creates 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>
Creates 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 |
Creates 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>
Complete Dialog Example
<%= 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 %>
Drawer
Drawers are slide-out panels typically used for filters, secondary forms, or detailed views without leaving the current page.
Basic Usage
<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>
Creates 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>
Creates 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 |
Creates a “Discard” button that closes the drawer.
<%= drawer_discard_button %>
| Parameter | Type | Default | Description |
|---|
controller_name | String | drawer | Stimulus controller name |
Icon
Icons are rendered using the Tabler Icons library.
Basic Usage
<%= icon('package') %>
<%= icon('shopping-cart', class: 'text-primary') %>
<%= icon('check', class: 'text-success', height: 24) %>
Options
| Option | Type | Default | Description |
|---|
class | String | - | Additional CSS classes |
height | Integer | - | Icon size in pixels |
style | String | - | Additional inline styles |
Output
<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>
Legacy Icon Names
For backwards compatibility, legacy icon names are automatically translated:
| Legacy Name | Tabler Icon |
|---|
save | device-floppy |
edit | pencil |
delete | trash |
add | plus |
cancel | x |
Common Icons
| Icon | Name | Usage |
|---|
| pencil | Edit actions |
| trash | Delete actions |
| plus | Add/create actions |
| eye | View/preview |
| download | Download/export |
| upload | Upload/import |
| check | Success/confirm |
| x | Close/cancel |
| dots-vertical | More options |
| chevron-down | Expand |
| chevron-right | Navigate |
Image
Displays optimized images with automatic WebP conversion and retina support.
Basic Usage
<%= 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') %>
Options
| 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 |
Features
- Retina support - Automatically scales dimensions by 2x for sharp display
- WebP conversion - Outputs optimized WebP format
- Smart cropping - When both dimensions provided, crops to fill
- Aspect ratio preservation - When one dimension provided, maintains ratio
Examples
<%# Product thumbnail in index table %>
<% if product.images.any? %>
<%= spree_image_tag product.images.first, 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' %>
With Placeholder
<% if @brand.logo.attached? %>
<%= spree_image_tag @brand.logo, width: 100, height: 100 %>
<% else %>
<div class="placeholder-image">
<%= icon('photo') %>
</div>
<% end %>
For comprehensive documentation on image handling, storage configuration, and best practices, see the Images & Assets guide.
Tooltips provide additional context on hover.
Basic Usage
<span data-controller="tooltip">
<%= icon('info-circle') %>
<%= tooltip('This is helpful information') %>
</span>
Creates 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_bubble
Creates 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>
Active Badge
Displays a status badge indicating active/inactive state.
Basic Usage
<%= active_badge(product.active?) %>
<%= active_badge(user.confirmed?) %>
<%= active_badge(order.paid?, label: 'Paid') %>
Options
| Option | Type | Default | Description |
|---|
condition | Boolean | required | The condition to evaluate |
label | String | Yes/No | Custom label text |
Output
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>
Custom Labels
<%= active_badge(subscription.active?, label: subscription.active? ? 'Active' : 'Expired') %>
<%= active_badge(feature.enabled?, label: feature.enabled? ? 'Enabled' : 'Disabled') %>
Avatar
Renders a user avatar with automatic fallback to initials.
Basic Usage
<%= render_avatar(current_user) %>
<%= render_avatar(user, width: 48, height: 48) %>
<%= render_avatar(admin, class: 'avatar-lg') %>
Options
| 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 |
Behavior
- If user has an attached avatar image → displays the image
- Otherwise → displays user’s initials on a colored background
<!-- With avatar image -->
<img src="avatar.jpg" class="avatar" style="width: 128px; height: 128px;">
<!-- Without avatar (fallback) -->
<div class="avatar" style="width: 128px; height: 128px;">JD</div>
Clipboard
Copy-to-clipboard functionality with visual feedback.
Basic Usage
<%= clipboard_component(product.sku) %>
<%= clipboard_component(api_key) %>
clipboard_component
Creates 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>
Creates just the copy button (for custom layouts).
<div data-controller="clipboard">
<input type="text" data-clipboard-target="source" value="custom-value">
<%= clipboard_button %>
</div>
Inline with Text
<div class="d-flex align-items-center gap-2">
<code><%= product.sku %></code>
<%= clipboard_component(product.sku) %>
</div>
Progress Bar
Displays a progress bar with customizable range.
Basic Usage
<%= progress_bar_component(75) %>
<%= progress_bar_component(150, max: 200) %>
<%= progress_bar_component(50, min: 0, max: 100) %>
Options
| Option | Type | Default | Description |
|---|
value | Integer | required | Current progress value |
min | Integer | 0 | Minimum value |
max | Integer | 100 | Maximum value |
Output
<div class="progress">
<div class="progress-bar"
role="progressbar"
style="width: 75%"
aria-valuenow="75"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
Examples
<!-- 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) %>
Date & Time
Helpers for displaying dates and times in the user’s local timezone.
spree_date
Renders a date in the user’s local format.
<%= spree_date(order.created_at) %>
<%= spree_date(product.available_on) %>
spree_time
Renders a date and time in the user’s local format.
<%= spree_time(order.completed_at) %>
<%= spree_time(shipment.shipped_at) %>
spree_time_ago
Renders 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_time
The 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') %>
Comparison
| 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 |
Best Practices
Use semantic components - Choose the right component for the interaction (Dialog for focused tasks, Drawer for contextual panels)
Provide feedback - Use tooltips and badges to give users context about their actions
Keep dropdowns focused - Limit dropdown menus to related actions, use dividers to group items
Use appropriate icons - Choose icons that clearly represent the action
Handle loading states - Use turbo_save_button_tag for forms to show loading feedback
Consider accessibility - Components include ARIA attributes and keyboard navigation