Rapid Rails UI Pro

Upgrade to unlock this component

Get Pro →

Steps

A multi-step wizard component with Turbo Frame integration for smooth step transitions, supporting horizontal, vertical, and minimal layout variants.

Key Features

  • Seven Variants - Horizontal, vertical, minimal, progress, breadcrumb, timeline, and vertical cards
  • Layout Slots - Flexible header, footer, and navigation slots for full customization
  • Flexible API - Positional or keyword title syntax for cleaner code
  • Navigation Modes - Linear, free navigation, or completed-only
  • Step States - Completed (checkmark), active (highlighted), pending, and error
  • Turbo Frame Integration - Smooth step transitions without full page reloads
  • LocalStorage Caching - Instant UI restore on page reload
  • Client-Side Validation - HTML5 validation before step transitions
  • Error State Handling - Display server-side validation errors on steps
  • Keyboard Navigation - Arrow keys, Home, End, Enter, and Space
  • Accessibility - Full ARIA support following WAI-ARIA Wizard patterns
  • JavaScript API - Programmatic control with Stimulus actions and events

Basic Usage

Create a multi-step wizard using the block syntax. Each step is defined with a title and content. Try the interactive demo below!

Set up your account credentials.

Basic Usage - Complete Implementation
<turbo-frame id="registration-frame">
  <%= rui_steps(
    id: "registration-wizard",
    url: wizards_path,
    current_step: @current_step,
    turbo_frame_id: frame_id,
    variant: :horizontal,
    navigation: :linear
  ) do |steps| %>

    <% steps.with_step(title: "Account") do %>
      <div class="py-6 space-y-4 max-w-md">
        <%= form_with(local: true) do |f| %>
          <%= f.rui_input(:email, label: "Email", type: :email, required: true) %>
          <%= f.rui_input(:password, label: "Password", type: :password, required: true) %>
        <% end %>
      </div>
    <% end %>

    <% steps.with_step(title: "Profile") do %>
      <div class="py-6 space-y-4 max-w-md">
        <%= form_with(local: true) do |f| %>
          <%= f.rui_input(:full_name, label: "Full Name", required: true) %>
          <%= f.rui_textarea(:bio, label: "Bio", rows: 4) %>
        <% end %>
      </div>
    <% end %>

    <% steps.with_step(title: "Confirm") do %>
      <div class="py-6 max-w-md">
        <p class="text-zinc-600 dark:text-zinc-400 mb-4">Review your information and click Submit to complete registration.</p>
      </div>
    <% end %>

  <% end %>
</turbo-frame>

Variants

Choose from seven layout variants based on your use case and available space. Each variant is optimized for different workflow patterns and screen sizes.

Horizontal (Default)

Progress indicators at top, content below. Best for short wizards with 3-5 steps on desktop.

Currently on Step 1

Vertical

Sidebar navigation on left, content on right. Best for complex wizards with many steps or detailed descriptions.

Account Information

Minimal

Simple "Step X of Y: Title" header. Best for embedding in dialogs or compact spaces.

Step 1 of 3:Connect
Connect

Connect your account to get started

Progress

Compact icon-only circles for space-constrained UIs. Best for mobile apps and narrow sidebars.

Account setup with user icon

Breadcrumb

Badge-style indicators with chevron separators. Familiar navigation pattern similar to breadcrumb trails.

Breadcrumb navigation style:

Timeline

Traditional timeline with left border spine. Best for process documentation and historical step tracking.

Timeline layout:

Set up your account credentials.

Vertical Cards

Full-width clickable cards in vertical stack. Best for mobile-first designs and step selection interfaces.

Vertical cards layout:

Set up your account credentials.

Horizontal Variant - Desktop Default
<turbo-frame id="wizard-frame">
  <%= rui_steps(
    id: "setup-wizard",
    url: wizard_path,
    current_step: @current_step,
    turbo_frame_id: frame_id,
    variant: :horizontal,
    navigation: :linear
  ) do |steps| %>
    <% steps.with_step(title: "Account") do %>
      <div class="py-6 space-y-4 max-w-md">
        <%= form_with(local: true) do |f| %>
          <%= f.rui_input(:email, label: "Email", required: true) %>
        <% end %>
      </div>
    <% end %>

    <% steps.with_step(title: "Profile") do %>
      <div class="py-6 space-y-4 max-w-md">
        <%= form_with(local: true) do |f| %>
          <%= f.rui_input(:name, label: "Full Name", required: true) %>
        <% end %>
      </div>
    <% end %>
  <% end %>
</turbo-frame>
Vertical Variant - Complex Workflows
<turbo-frame id="wizard-frame">
  <%= rui_steps(
    id: "setup-wizard",
    url: wizard_path,
    current_step: @current_step,
    turbo_frame_id: frame_id,
    variant: :vertical,
    navigation: :linear
  ) do |steps| %>
    <% steps.with_step(title: "Account", description: "Create your account") do %>
      <div class="py-6 space-y-4 max-w-md">
        Account setup content...
      </div>
    <% end %>

    <% steps.with_step(title: "Details", description: "Add your profile") do %>
      <div class="py-6 space-y-4 max-w-md">
        Profile details content...
      </div>
    <% end %>
  <% end %>
</turbo-frame>
Minimal Variant - Compact/Dialog Use
<turbo-frame id="wizard-frame">
  <%= rui_steps(
    id: "compact-wizard",
    url: wizard_path,
    current_step: @current_step,
    turbo_frame_id: frame_id,
    variant: :minimal,
    navigation: :linear
  ) do |steps| %>
    <% steps.with_step(title: "Step 1") { "Content for step 1..." } %>
    <% steps.with_step(title: "Step 2") { "Content for step 2..." } %>
  <% end %>
</turbo-frame>
Progress Variant - Icon-Only Compact
<turbo-frame id="wizard-frame">
  <%= rui_steps(
    id: "progress-wizard",
    url: wizard_path,
    current_step: @current_step,
    turbo_frame_id: frame_id,
    variant: :progress,
    navigation: :linear
  ) do |steps| %>
    <% steps.with_step(title: "Account", icon: :user) { "..." } %>
    <% steps.with_step(title: "Details", icon: :identification) { "..." } %>
    <% steps.with_step(title: "Confirm", icon: :check) { "..." } %>
  <% end %>
</turbo-frame>

Variant Selection Tips

  • Desktop users with 3-5 steps: Use Horizontal
  • Complex workflows with descriptions: Use Vertical or Timeline
  • Mobile-first or narrow spaces: Use Progress or Vertical Cards
  • Dialog wizards: Use Minimal
  • Navigation-style UI: Use Breadcrumb

Step States

Steps automatically display their state based on the current position and any errors.

Completed

Green checkmark

2

Active

Primary with ring

3

Pending

Gray, muted

!

Error

Red indicator

Error State
<%# Show error state on a specific step %>
<%= rui_steps(
  id: "wizard",
  url: path,
  current_step: @current_step,
  error_step: @error_step  # Set when server validation fails
) do |steps| %>
  ...
<% end %>

Layout Slots

Customize the wizard layout with header and footer slots for additional content, plus position controls for indicators and buttons.

Header Slot with_header
Step Indicators indicators_position: :top
Step Content
Nav Buttons buttons_position: :bottom
Footer Slot with_footer

Position Controls

Use these params to control where built-in elements appear:

  • indicators_position - Where step indicators appear: :top (default), :bottom, or :none
  • buttons_position - Where nav buttons appear: :top, :bottom (default), or :none
Slot Method Position Use Case
Header with_header Above everything Title, description, cancel button
Footer with_footer Below everything Help links, terms, progress info

Header Slot

Add custom content above the step indicators. Great for wizard title, description, or cancel buttons.

Account Setup

Complete these steps to activate your account

Set up your account credentials

Header Slot
<%= rui_steps(id: "wizard", url: wizard_path) do |steps| %>
  <% steps.with_header do %>
    <div class="flex justify-between items-center pb-4 border-b border-zinc-200">
      <div>
        <h2 class="text-xl font-semibold">Account Setup</h2>
        <p class="text-sm text-zinc-500">Complete these steps to activate your account</p>
      </div>
      <%= rui_button("Cancel", variant: :ghost, size: :sm) %>
    </div>
  <% end %>

  <% steps.with_step("Account") { "..." } %>
  <% steps.with_step("Profile") { "..." } %>
<% end %>

Footer Slot

Add custom content below the wizard. Great for help links, terms, or progress indicators.

Footer Slot
<%= rui_steps(id: "wizard", url: wizard_path) do |steps| %>
  <% steps.with_step("Account") { "..." } %>
  <% steps.with_step("Profile") { "..." } %>

  <% steps.with_footer do %>
    <div class="flex justify-between items-center text-sm text-zinc-500">
      <span>Your progress is saved automatically</span>
      <a href="/help" class="text-blue-600 hover:underline">Need help?</a>
    </div>
  <% end %>
<% end %>

Swapping Positions

Use position params to swap where indicators and buttons appear.

Buttons at top, indicators at bottom:

Account content - notice buttons at top!

Indicators at Bottom, Buttons at Top
<%= rui_steps(
  id: "wizard",
  url: wizard_path,
  indicators_position: :bottom,
  buttons_position: :top
) do |steps| %>
  <% steps.with_step("Account") { "..." } %>
  <% steps.with_step("Profile") { "..." } %>
<% end %>

Custom Navigation

Hide default buttons and add custom navigation in the header or footer.

Setup Progress

Custom buttons are in the header!

Custom Navigation in Header
<%= rui_steps(
  id: "compact-wizard",
  url: wizard_path,
  buttons_position: :none  <%# Hide default buttons %>
) do |steps| %>
  <% steps.with_header do %>
    <div class="flex justify-between items-center">
      <span class="font-medium">Setup Progress</span>
      <div class="flex gap-2">
        <%= rui_button("Back", size: :sm, variant: :outline, data: { action: "rapid-rails-ui--steps#back" }) %>
        <%= rui_button("Skip", size: :sm, variant: :ghost, data: { action: "rapid-rails-ui--steps#next" }) %>
        <%= rui_button("Next", size: :sm, data: { action: "rapid-rails-ui--steps#next" }) %>
      </div>
    </div>
  <% end %>

  <% steps.with_step("Step 1") { "Content..." } %>
  <% steps.with_step("Step 2") { "Content..." } %>
<% end %>

Stimulus Actions for Custom Buttons

Action Description
rapid-rails-ui--steps#back Go to previous step
rapid-rails-ui--steps#next Go to next step (validates first)
rapid-rails-ui--steps#goToStep Jump to step (needs data-step)

Both Slots Together

Use both slots together for complete layout control.

Welcome to Acme

Let's get you set up

×

Create your account credentials

Progress saved automatically Need help?
Header and Footer Slots
<%= rui_steps(id: "onboarding", url: onboarding_path, color: :blue) do |steps| %>
  <% steps.with_header do %>
    <div class="flex justify-between items-center pb-4 border-b">
      <div>
        <h2 class="text-xl font-bold">Welcome to Acme</h2>
        <p class="text-sm text-zinc-500">Let's get you set up</p>
      </div>
      <a href="/" class="text-zinc-400 hover:text-zinc-600">&times;</a>
    </div>
  <% end %>

  <% steps.with_step("Account", icon: :user) do %>
    <%= rui_input(type: :email, label: "Email", required: true) %>
    <%= rui_input(type: :password, label: "Password", required: true) %>
  <% end %>

  <% steps.with_step("Profile", icon: :identification) do %>
    <%= rui_input(label: "Full Name", required: true) %>
  <% end %>

  <% steps.with_step("Done", icon: :check_circle) do %>
    <div class="text-center py-8">
      <p class="text-green-600 font-medium">You're all set!</p>
    </div>
  <% end %>

  <% steps.with_footer do %>
    <div class="flex justify-between text-sm text-zinc-400 pt-4 border-t">
      <span>Progress saved automatically</span>
      <a href="/help" class="text-blue-500 hover:underline">Need help?</a>
    </div>
  <% end %>
<% end %>

Works With All Variants

Layout slots are supported across all 7 variants: horizontal, vertical, minimal, progress, breadcrumb, timeline, and vertical_cards.

Step Options

Configure individual steps with icons, descriptions, and optional markers.

With Icons

Account setup with user icon

Step Options
<%# With icons instead of numbers %>
<% steps.with_step(title: "Account", icon: :user) do %>
  ...
<% end %>

<%# With descriptions (shown in vertical variant) %>
<% steps.with_step(title: "Payment", description: "Credit card or PayPal") do %>
  ...
<% end %>

<%# Mark step as optional %>
<% steps.with_step(title: "Extras", optional: true) do %>
  ...
<% end %>

Custom Button Text

Customize the navigation button labels to match your use case.

First step - notice the button labels

Custom Button Text
<%= rui_steps(
  id: "wizard",
  url: path,
  back_text: "Previous",
  next_text: "Continue",
  submit_text: "Finish",
  loading_text: "Processing..."
) do |steps| %>
  ...
<% end %>

Controller Setup

Handle step transitions in your Rails controller. The component sends a 'direction' parameter.

Rails Controller
class WizardsController < ApplicationController
  before_action :set_wizard

  def show
    render partial: "step_#{@wizard.current_step}"
  end

  def update
    case params[:direction]
    when "next"
      if @wizard.valid_for_step?(@wizard.current_step)
        @wizard.increment!(:current_step)
      else
        flash.now[:alert] = "Please fix errors before continuing"
      end
    when "back"
      @wizard.decrement!(:current_step)
    when /^goto:(\d+)$/
      target_step = $1.to_i
      @wizard.update!(current_step: target_step) if can_navigate_to?(target_step)
    end

    render partial: "step_#{@wizard.current_step}"
  end

  private

  def set_wizard
    @wizard = Wizard.find_or_create_by(session_id: session.id)
  end

  def can_navigate_to?(step)
    step <= @wizard.current_step + 1
  end
end

LocalStorage Caching

The component caches the current step in LocalStorage for instant UI restore on page reload. Server state always takes precedence.

LocalStorage Options
<%# Disable caching %>
<%= rui_steps(id: "wizard", url: path, cache_locally: false) do |steps| %>
  ...
<% end %>

<%# Storage key format: rui_steps_{id}_current %>

<%# Clear cache programmatically (in your Stimulus controller): %>
// this.stepsController.clearStorage()

Client-Side Validation

HTML5 validation runs before step transitions. Users can always go back without validation.

Validation Options
<%# Disable client-side validation %>
<%= rui_steps(id: "wizard", url: path, client_validation: false) do |steps| %>
  ...
<% end %>

<%# Validation behavior: %>
<%# - Next button: validates current step's inputs before proceeding %>
<%# - Back button: skips validation, allows going back with invalid data %>
<%# - Server validation still runs regardless of this setting %>

Real-World Examples

Post Creation Wizard

This is a fully functional wizard that creates a real Post. Fill in each step and click 'Create Post' to publish. Your data is saved as you navigate between steps.

Post Details

Status
Post Creation Wizard - Complete Implementation
<turbo-frame id="post-wizard-frame">
  <% if @post.published? %>
    <div class="py-6 space-y-4 text-center">
      <%= rui_alert(type: :success, title: "Post Published!", dismissible: false) %>
    </div>
  <% else %>
    <%= rui_steps(
      id: "post-wizard",
      url: posts_path,
      current_step: @wizard_step || 0,
      turbo_frame_id: frame_id,
      variant: :horizontal,
      navigation: :free,
      submit_text: "Publish Post"
    ) do |steps| %>

      <% steps.with_step(title: "Basic Info", icon: :file_text) do %>
        <div class="py-6 space-y-4 max-w-lg">
          <%= form_with(local: true) do |f| %>
            <%= f.rui_input(:title,
              label: "Title",
              placeholder: "Enter post title",
              value: @post.title,
              required: true) %>
            <%= f.rui_textarea(:excerpt,
              label: "Excerpt",
              placeholder: "Brief summary",
              value: @post.excerpt,
              rows: 3) %>
            <%= f.rui_select(:status,
              label: "Status",
              collection: ["draft", "published", "archived"],
              selected: @post.status) %>
          <% end %>
        </div>
      <% end %>

      <% steps.with_step(title: "Content", icon: :edit) do %>
        <div class="py-6 space-y-4 max-w-lg">
          <%= form_with(local: true) do |f| %>
            <%= f.rui_textarea(:body,
              label: "Body",
              placeholder: "Write your post content...",
              value: @post.body,
              rows: 10) %>
          <% end %>
        </div>
      <% end %>

      <% steps.with_step(title: "Settings", icon: :settings) do %>
        <div class="py-6 space-y-4 max-w-lg">
          <%= form_with(local: true) do |f| %>
            <%= f.rui_checkbox(:featured,
              label: "Featured post",
              checked: @post.featured?) %>
            <%= f.rui_checkbox(:allow_comments,
              label: "Allow comments",
              checked: @post.allow_comments?) %>
          <% end %>
        </div>
      <% end %>

      <% steps.with_step(title: "Review", icon: :check_circle) do %>
        <div class="py-6 max-w-lg">
          <div class="bg-zinc-50 dark:bg-zinc-800 p-4 rounded-lg space-y-2 mb-4">
            <p><strong>Title:</strong> <%= @post.title %></p>
            <p><strong>Status:</strong> <%= @post.status %></p>
            <p><strong>Featured:</strong> <%= @post.featured? ? "Yes" : "No" %></p>
          </div>
          <p class="text-zinc-600 dark:text-zinc-400">Click Publish to save your post.</p>
        </div>
      <% end %>

    <% end %>
  <% end %>
</turbo-frame>

User Onboarding Flow

Step 1 of 3:Welcome
Welcome

Welcome aboard!

Let's get you set up in just a few steps.

StockLive Bidder Registration (6-Step Wizard)

A complex multi-step wizard for livestock auction bidder registration. Demonstrates handling of form data, combobox selections, and database persistence across 6 steps.

StockLive Bidder Registration - 6-Step Pattern
<turbo-frame id="stocklive-registration-frame">
  <% if @registration.completed? %>
    <div class="text-center py-8">
      <%= rui_icon(:check, size: :lg, class: "inline text-green-600 mb-4") %>
      <h3 class="text-2xl font-bold mb-2">Registration Complete!</h3>
      <p class="text-zinc-600 dark:text-zinc-400 mb-6">Your bidder registration has been saved successfully.</p>
    </div>
  <% else %>
    <%= rui_steps(
      id: "stocklive-wizard",
      url: stocklive_wizard_path,
      current_step: @wizard_step || 0,
      turbo_frame_id: frame_id,
      variant: :horizontal,
      navigation: :linear,
      submit_text: "Complete Registration",
      back_text: "Back",
      next_text: "Continue"
    ) do |steps| %>

      <% steps.with_step(title: "Sale", icon: :tag) do %>
        <div class="py-6 space-y-4 max-w-lg">
          <%= rui_text("Select Your Sale", size: :lg, weight: :semibold, class: "mb-4") %>
          <%= form_with(local: true) do |f| %>
            <%= f.rui_combobox(:sale_id, label: "Sale Event", collection: SALES_OPTIONS, required: true) %>
            <%= f.rui_combobox(:account_type, label: "Account Type", collection: ACCOUNT_TYPES, required: true) %>
          <% end %>
        </div>
      <% end %>

      <% steps.with_step(title: "Personal", icon: :user) do %>
        <div class="py-6 space-y-4 max-w-lg">
          <%= rui_text("Personal Details", size: :lg, weight: :semibold, class: "mb-4") %>
          <%= form_with(local: true) do |f| %>
            <div class="grid grid-cols-2 gap-4">
              <%= f.rui_input(:first_name, label: "First Name", required: true) %>
              <%= f.rui_input(:last_name, label: "Last Name", required: true) %>
            </div>
            <%= f.rui_input(:email, label: "Email", type: :email, required: true) %>
            <%= f.rui_input(:mobile, label: "Mobile", type: :tel, required: true) %>
          <% end %>
        </div>
      <% end %>

      <% steps.with_step(title: "Address", icon: :map_pin) do %>
        <div class="py-6 space-y-4 max-w-lg">
          <%= rui_text("Address Details", size: :lg, weight: :semibold, class: "mb-4") %>
          <%= form_with(local: true) do |f| %>
            <div class="grid grid-cols-2 gap-4">
              <%= f.rui_select(:country, label: "Country", collection: COUNTRIES, required: true) %>
              <%= f.rui_select(:state, label: "State", collection: STATES, required: true) %>
            </div>
            <%= f.rui_input(:street_address, label: "Street Address", required: true) %>
            <div class="grid grid-cols-2 gap-4">
              <%= f.rui_input(:suburb, label: "Suburb", required: true) %>
              <%= f.rui_input(:postcode, label: "Postcode", required: true) %>
            </div>
          <% end %>
        </div>
      <% end %>

      <% steps.with_step(title: "Business", icon: :briefcase) do %>
        <div class="py-6 space-y-4 max-w-lg">
          <%= rui_text("Business Details", size: :lg, weight: :semibold, class: "mb-4") %>
          <%= form_with(local: true) do |f| %>
            <%= f.rui_input(:trading_name, label: "Trading Name", required: true) %>
            <div class="grid grid-cols-2 gap-4">
              <%= f.rui_input(:pic, label: "PIC Number", required: true, maxlength: 8) %>
              <%= f.rui_input(:abn, label: "ABN", required: true) %>
            </div>
          <% end %>
        </div>
      <% end %>

      <% steps.with_step(title: "Agent", icon: :users) do %>
        <div class="py-6 space-y-4 max-w-lg">
          <%= rui_text("Agent Information", size: :lg, weight: :semibold, class: "mb-4") %>
          <%= form_with(local: true) do |f| %>
            <%= f.rui_input(:trading_agent, label: "Your Agent", required: true) %>
            <div class="grid grid-cols-2 gap-4">
              <%= f.rui_input(:selling_agent_name, label: "Agent Name", required: true) %>
              <%= f.rui_input(:selling_agent_phone, label: "Agent Phone", type: :tel, required: true) %>
            </div>
          <% end %>
        </div>
      <% end %>

      <% steps.with_step(title: "Review", icon: :check_circle) do %>
        <div class="py-6">
          <%= rui_text("Review Your Information", size: :lg, weight: :semibold, class: "mb-4") %>
          <div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4 space-y-3 mb-6">
            <div class="flex justify-between pb-3 border-b border-zinc-200 dark:border-zinc-700">
              <span class="text-sm text-zinc-500">Name:</span>
              <span class="font-medium"><%= [@registration.first_name, @registration.last_name].join(" ") %></span>
            </div>
            <div class="flex justify-between pb-3 border-b border-zinc-200 dark:border-zinc-700">
              <span class="text-sm text-zinc-500">Email:</span>
              <span class="font-medium"><%= @registration.email %></span>
            </div>
            <div class="flex justify-between">
              <span class="text-sm text-zinc-500">Trading Name:</span>
              <span class="font-medium"><%= @registration.trading_name %></span>
            </div>
          </div>
          <p class="text-zinc-600 dark:text-zinc-400">Click 'Complete Registration' to save your bidder registration.</p>
        </div>
      <% end %>

    <% end %>
  <% end %>
</turbo-frame>
posts_controller.rb - Handle Wizard Steps
class PostsController < ApplicationController
  def wizard
    @post = Post.find_or_create_by(user_id: current_user.id, demo: true)
    @wizard_step = session[:post_wizard_step] || 0
    @wizard_total_steps = 4

    case params[:direction]
    when "next"
      # Update post with current step data
      if @post.update(post_params)
        if @wizard_step >= @wizard_total_steps - 1
          # Final step - publish and clear session
          @post.update(published_at: Time.current)
          session.delete(:post_wizard_step)
          @completed = true
        else
          # Move to next step
          @wizard_step = [@wizard_step + 1, @wizard_total_steps - 1].min
          session[:post_wizard_step] = @wizard_step
        end
      end

    when "back"
      # Save current step, move to previous
      @post.update(post_params) if post_params.present?
      @wizard_step = [@wizard_step - 1, 0].max
      session[:post_wizard_step] = @wizard_step

    when /^goto:(\d+)$/
      # Jump to specific step
      @post.update(post_params) if post_params.present?
      target = $1.to_i
      @wizard_step = [[target, 0].max, @wizard_total_steps - 1].min
      session[:post_wizard_step] = @wizard_step
    end

    # Render partial with turbo-frame response
    render partial: "posts/wizard",
           locals: {
             post: @post,
             wizard_step: @wizard_step,
             completed: @completed || false
           },
           layout: false
  end

  private

  def post_params
    params.require(:post).permit(:title, :body, :excerpt, :status, :featured, :allow_comments)
  end
end
routes.rb - Add Wizard Route
Rails.application.routes.draw do
  # POST to same endpoint handles wizard navigation
  post "/posts/wizard", to: "posts#wizard", as: "posts_wizard"
  resources :posts
end

Key Points

  • Session tracking - Store current step in session
  • Persistent updates - Save data on each step transition
  • Direction parameter - Component sends params[:direction] (next, back, goto:N)
  • Partial rendering - Return HTML partial for Turbo frame update
  • Frame targeting - Form targets outer turbo-frame for full component re-render

✓ Copy-Paste Ready Implementation Steps

  1. Create a view partial (e.g., _wizard.html.erb)
    • Wrap rui_steps component in a <turbo-frame>
    • Pass turbo_frame_id to the component (outer frame mode)
    • See examples above for complete implementations
  2. Create controller action to handle wizard navigation
    • Store current_step in session
    • Handle params[:direction] (next, back, goto:N)
    • Update model data on each step
    • Render partial with layout: false for Turbo
  3. Add route that POSTs to your wizard action
    • post "/wizards/complete", to: "wizards#complete"
    • Component sends form submissions to this URL
  4. Test in browser
    • Navigate through steps - indicators update
    • Fill form fields and go back - data persists
    • No full page reloads (Turbo handles updates)
    • Open browser console - no errors should appear

🎯 Quick Reference

  • Indicators not updating? Make sure you're using outer frame mode (pass turbo_frame_id parameter)
  • Form not submitting? Verify your route exists and accepts POST requests
  • Data not persisting? Save form params to database in controller action
  • Console errors? Check that Stimulus controller is loaded (included in gem automatically)
  • Copy-paste example code? All code blocks on this page are production-ready

✅ OUTER FRAME MODE (Recommended for Wizards)

Use this when you need indicators to update on step transitions.

Outer Frame Mode Pattern
<turbo-frame id="wizard-frame">
  <%= rui_steps(
    id: "my-wizard",
    url: wizard_path,
    current_step: @current_step,
    turbo_frame_id: frame_id  # ← KEY: Pass outer frame ID
  ) do |steps| %>
    <% steps.with_step(title: "Step 1") do %>
      Step 1 content
    <% end %>
  <% end %>
</turbo-frame>
  • Wrap component in turbo-frame in your partial
  • Pass turbo_frame_id to component
  • Form targets the OUTER frame (full component re-render)
  • Stimulus controller gets new current-value
  • ✅ Indicators update from gray to green
  • ✅ Content updates
  • ✅ All 7 variants work identically

INNER FRAME MODE (Default)

Used automatically if you don't pass turbo_frame_id.

Inner Frame Mode (Default)
<%= rui_steps(
  id: "my-wizard",
  url: wizard_path,
  current_step: @current_step
  # turbo_frame_id NOT passed = inner frame mode
) do |steps| %>
  ...
<% end %>
  • Component creates internal turbo-frame automatically
  • Form targets the INNER frame (content-only update)
  • Indicators DON'T update (frame doesn't re-render component wrapper)
  • Content updates only
  • Use when: embedding in existing frames or simple demos

🎯 Quick Decision

Use Outer Frame Mode if:

  • Building a real wizard form
  • You want visual indicators (steps) to update
  • You're in a partial or page view (not embedded)

Example: Post Wizard, StockLive Registration, Onboarding → All use Outer Frame Mode

Accessibility

The Steps component follows WAI-ARIA Wizard patterns for accessible multi-step forms.

ARIA Attributes

  • aria-label="Progress" on the navigation element
  • role="list" on the step indicator list
  • aria-current="step" marks the active step
  • aria-disabled="true" on locked/unavailable steps
  • aria-labelledby on step content references step title

Keyboard Navigation

  • Arrow Right/Down moves focus to next step indicator
  • Arrow Left/Up moves focus to previous step indicator
  • Home / End jumps to first/last step
  • Enter or Space activates focused step (if allowed)
  • Tab navigates between form elements within step content

Semantic HTML

  • Clickable step indicators use <button> for proper interaction
  • Non-clickable indicators (linear mode) use <span> to indicate no interaction
  • Step content wrapped in role="group" with proper labeling
  • Decorative SVG icons have aria-hidden="true"

JavaScript API

The Steps component exposes Stimulus actions and custom events for programmatic control.

Stimulus Actions

Use these actions via data-action attributes:

Action Description
steps#next Navigate to next step (validates if enabled)
steps#back Navigate to previous step
steps#goToStep Jump to specific step (respects navigation mode)
steps#handleKeydown Keyboard navigation handler

Public Methods

Method Description
setError(stepIndex) Set error state on a specific step
clearError(stepIndex) Clear error from a specific step
clearAllErrors() Clear all error states
clearStorage() Clear LocalStorage cache

Custom Events

Listen to these events for lifecycle hooks:

Event When Detail
steps:before-navigate Before step transition { from, to }
steps:after-navigate After step transition { from, to }
steps:step-changed Step changed { step }
steps:validation-failed Client validation failed { step, errors }
steps:error-set Error state set { step }
steps:error-cleared Error state cleared { step }
Listening to Events
<%# Listen for step changes %>
<div data-controller="my-controller"
    data-action="steps:step-changed->my-controller#onStepChange
                  steps:validation-failed->my-controller#onValidationFailed">
  <%= rui_steps(id: "wizard", url: path) do |steps| %>
    ...
  <% end %>
</div>
Event Handler Example
// my_controller.js
onStepChange(event) {
  const { step } = event.detail
  console.log(`Now on step ${step}`)
}

onValidationFailed(event) {
  const { step, errors } = event.detail
  console.log(`Validation failed on step ${step}`)
}
Programmatic Control
// Get the steps controller
const stepsElement = document.querySelector('[data-controller*="steps"]')
const stepsController = application.getControllerForElementAndIdentifier(
  stepsElement,
  "rapid-rails-ui--steps"
)

// Set/clear errors
stepsController.setError(0)      // Mark step 0 as having an error
stepsController.clearError(0)    // Clear error from step 0
stepsController.clearAllErrors() // Clear all errors

// Clear cached state
stepsController.clearStorage()

CSS Architecture

Connector Line Classes

The Steps component renders connecting lines between step indicators. These lines use Tailwind utility classes that must be explicitly safelisted to appear in production CSS builds.

Horizontal connectors: h-px h-0.5 h-1

Vertical connectors: w-px w-0.5 w-1

Connector colors: bg-{color}-{200..600}

Dark mode colors: dark:bg-{color}-{600..800}

Safelist Configuration

These classes are registered in app/assets/tailwind/safelist.css to ensure they appear in production CSS. If connector lines don't appear:

  1. Verify safelist includes connector classes
  2. Rebuild CSS: bin/rails assets:clobber && bin/rails assets:precompile
  3. Clear browser cache and reload

Tailwind Compilation Issue

Tailwind's content scanner sometimes misses dynamically-sized utilities like h-0.5. The safelist explicitly tells Tailwind to generate these classes so they're always available, even if not found in source files.

API Reference

rui_steps

Multi-step wizard with Turbo Frame integration

Parameter Type Default Description
id* String Unique identifier for the wizard
url* String Form submission URL
current_step Integer 0 Current step index (0-based)
variant Symbol :horizontal Layout variant (:horizontal, :vertical, :minimal, :progress, :breadcrumb, :timeline, :vertical_cards)
navigation Symbol :linear Navigation mode (:linear, :free, :completed_only)
color Symbol :zinc Theme color for active indicators (:zinc, :primary, :blue, :green, :red, etc.)
size Symbol :base Size variant (:sm, :base, :lg)
indicators_position Symbol :top Position of step indicators (:top, :bottom, :none)
buttons_position Symbol :bottom Position of navigation buttons (:top, :bottom, :none)
cache_locally Boolean true Cache current step in LocalStorage
client_validation Boolean true Run HTML5 validation before step transitions
error_step Integer Step index with server-side validation error
back_text String "Back" Back button label
next_text String "Next" Next button label
submit_text String "Submit" Submit button label (shown on last step)
loading_text String "Saving..." Button text during Turbo submission
class String Custom CSS classes

Slots

Content slots for customizing component parts

Slot Description
step Define individual wizard steps using with_step(). Title can be positional or keyword: with_step('Title') or with_step(title: 'Title').
header Custom content rendered above step indicators. Great for wizard title, description, or cancel button.
footer Custom content rendered below everything. Great for help links, terms, or progress info.

Related Components