Skip to main content
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.

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
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 %>
OptionTypeDefaultDescription
classString-Additional CSS classes
placementStringbottom-startMenu placement: bottom-start, bottom-end, top-start, top-end
directionString-Legacy option: leftbottom-end, toptop-start, top-lefttop-end
portalBoolean-Whether to portal the dropdown to body
dataHash-Additional data attributes
Creates the button that toggles the dropdown menu.
<%= dropdown_toggle class: 'btn-primary' do %>
  Actions <%= icon('chevron-down', class: 'ml-2') %>
<% end %>
OptionTypeDefaultDescription
classString-Additional CSS classes (added to btn base class)
dataHash-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 %>
OptionTypeDefaultDescription
classString-Additional CSS classes
dataHash-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>

dialog_header

Creates a dialog header with title and close button.
<%= dialog_header('Confirm Action') %>
<%= dialog_header('Custom Dialog', 'my-dialog') %>
ParameterTypeDefaultDescription
titleStringrequiredThe dialog title
controller_nameStringdialogStimulus 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_button

Creates a standalone close button for dialogs.
<%= dialog_close_button %>
<%= dialog_close_button('custom-controller') %>
ParameterTypeDefaultDescription
controller_nameStringdialogStimulus controller name

dialog_discard_button

Creates a “Discard” button that closes the dialog.
<%= dialog_discard_button %>
ParameterTypeDefaultDescription
controller_nameStringdialogStimulus 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>

drawer_header

Creates a drawer header with title and close button.
<%= drawer_header('Filters') %>
<%= drawer_header('Details', 'custom-drawer') %>
ParameterTypeDefaultDescription
titleStringrequiredThe drawer title
controller_nameStringdrawerStimulus 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_button

Creates a standalone close button for drawers.
<%= drawer_close_button %>
<%= drawer_close_button('custom-drawer') %>
ParameterTypeDefaultDescription
controller_nameStringdrawerStimulus controller name

drawer_discard_button

Creates a “Discard” button that closes the drawer.
<%= drawer_discard_button %>
ParameterTypeDefaultDescription
controller_nameStringdrawerStimulus 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

OptionTypeDefaultDescription
classString-Additional CSS classes
heightInteger-Icon size in pixels
styleString-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 NameTabler Icon
savedevice-floppy
editpencil
deletetrash
addplus
cancelx

Common Icons

IconNameUsage
pencilEdit actions
trashDelete actions
plusAdd/create actions
eyeView/preview
downloadDownload/export
uploadUpload/import
checkSuccess/confirm
xClose/cancel
dots-verticalMore options
chevron-downExpand
chevron-rightNavigate

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

OptionTypeDefaultDescription
imageAsset/AttachmentrequiredSpree::Asset or ActiveStorage attachment
widthInteger-Display width in pixels
heightInteger-Display height in pixels
classString-CSS classes
altString-Alt text for accessibility
loadingSymbol-: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.

Tooltip

Tooltips provide additional context on hover.

Basic Usage

<span data-controller="tooltip">
  <%= icon('info-circle') %>
  <%= tooltip('This is helpful information') %>
</span>

tooltip

Creates a tooltip container.
<%= tooltip('Simple text tooltip') %>

<%= tooltip do %>
  <strong>Rich content</strong> with <em>formatting</em>
<% end %>
ParameterTypeDescription
textStringTooltip 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') %>
ParameterTypeDefaultDescription
textStringrequiredTooltip text
placementStringtopTooltip position: top, bottom, left, right
cssStringtext-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

OptionTypeDefaultDescription
conditionBooleanrequiredThe condition to evaluate
labelStringYes/NoCustom 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

OptionTypeDefaultDescription
userObjectrequiredUser object (must respond to avatar and name)
widthInteger128Avatar width in pixels
heightInteger128Avatar height in pixels
classStringavatarCSS classes

Behavior

  1. If user has an attached avatar image → displays the image
  2. 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') %>
ParameterTypeDescription
textStringThe 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_button

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

OptionTypeDefaultDescription
valueIntegerrequiredCurrent progress value
minInteger0Minimum value
maxInteger100Maximum 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

HelperOutputUse 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