Stepper Rails Components

Stepper components for your Ruby on Rails application. Perfect for onboarding flows, registration forms, and checkout processes.

Installation

1. Stimulus Controller Setup

Start by adding the following controller to your project:

This code is available to Pro users only.

// Hey curious person!
// You seem to be sneaking around the code...
// I hope you're enjoying the components!
// Have a great day!

class SecretMessage {
  constructor() {
    this.message = "Thanks for checking out Rails Blocks!";
    this.compliment = "You're awesome!";
  }

  reveal() {
    console.log(this.message);
    return this.compliment;
  }
}

const secret = new SecretMessage();
secret.reveal();

2. Custom CSS

Here are the custom CSS classes that we used on Rails Blocks to style the form components. You can copy and paste these into your own CSS file to style & personalize your forms.

/* Forms */

label,
.label {
  @apply text-sm leading-6 font-medium text-neutral-700;
  @apply dark:text-neutral-100;
}

.form-input[disabled] {
  @apply cursor-not-allowed bg-neutral-200;
}

/* non-input elements (like the Stripe card form) can be styled to look like an input */
div.form-control {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background-color: #fff;
  border-width: 1px;
  padding-top: 0.5rem;
  padding-right: 0.75rem;
  padding-bottom: 0.5rem;
  padding-left: 0.75rem;
  font-size: 1rem;
  line-height: 1.5rem;
}

.form-control {
  @apply block w-full rounded-lg bg-white border-0 px-3 py-2 text-base leading-6 text-neutral-900 shadow-sm ring-1 ring-neutral-300 outline-hidden ring-inset placeholder:text-neutral-500 focus:ring-2 focus:ring-neutral-600 dark:bg-neutral-700 dark:text-white dark:placeholder-neutral-300 dark:ring-neutral-600 dark:focus:ring-neutral-500;
}

@media (min-width: 640px) {
  .form-control {
    font-size: 0.875rem; /* text-sm equivalent (14px) for larger screens */
  }
}

.form-control[disabled] {
  @apply cursor-not-allowed bg-neutral-100 dark:bg-neutral-600;
}

.form-control.error {
  @apply border-red-400 ring-red-300 focus:ring-red-500 dark:border-red-600 dark:ring-red-500;
}

select:not([multiple]) {
  @apply w-full appearance-none rounded-lg border-0 bg-white px-3 py-2 text-base leading-6 text-neutral-900 shadow-sm ring-1 ring-neutral-300 outline-hidden ring-inset focus:ring-2 focus:ring-neutral-600;

  /* Custom dropdown arrow */
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23737373' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
  background-position: right 0.75rem center;
  background-repeat: no-repeat;
  background-size: 1.25em 1.25em;
  padding-right: 2.5rem;
}

@media (min-width: 640px) {
  select:not([multiple]) {
    font-size: 0.875rem; /* text-sm equivalent (14px) for larger screens */
  }
}

/* Dark mode styling for single select */
.dark {
  select:not([multiple]) {
    @apply dark:bg-neutral-700 dark:text-white dark:ring-neutral-600 dark:focus:ring-neutral-500;
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23A1A1AA' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
  }
}

select:not([multiple])[disabled] {
  @apply cursor-not-allowed bg-neutral-100 opacity-75 ring-neutral-200 dark:bg-neutral-600 dark:ring-neutral-500;
}

select[multiple] {
  @apply w-full rounded-lg rounded-r-none border-0 bg-white px-3 py-2.5 text-base leading-6 text-neutral-900 shadow-sm outline-1 -outline-offset-1 outline-neutral-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-neutral-600 dark:outline-neutral-600;
  min-height: 120px;
}

select[multiple] option {
  @apply rounded-md;
}

@media (min-width: 640px) {
  select[multiple] {
    font-size: 0.875rem; /* text-sm equivalent (14px) for larger screens */
  }
}

/* Dark mode styling for multiple select */
.dark {
  select[multiple] {
    @apply dark:bg-neutral-700 dark:text-white dark:ring-neutral-600 dark:focus:ring-neutral-500;
  }
}

select[multiple][disabled] {
  @apply cursor-not-allowed bg-neutral-100 opacity-75 ring-neutral-200 dark:bg-neutral-600 dark:ring-neutral-500;
}

option {
  @apply bg-white px-3 py-2 text-sm text-neutral-900 dark:bg-neutral-700 dark:text-neutral-100;
}

option:checked {
  @apply bg-neutral-100 dark:bg-neutral-600;
}

option:hover {
  @apply bg-neutral-50 dark:bg-neutral-600;
}

.caret {
  @apply pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-neutral-800;
}

[type="checkbox"] {
  @apply size-4 cursor-pointer appearance-none rounded-sm border border-neutral-300 bg-white checked:border-neutral-700 checked:bg-neutral-800 indeterminate:border-neutral-700 indeterminate:bg-neutral-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:border-neutral-300 disabled:bg-neutral-100 disabled:checked:bg-neutral-100 dark:border-white/20 dark:bg-neutral-800 dark:checked:border-white/20 dark:checked:bg-neutral-900 dark:indeterminate:border-neutral-500 dark:indeterminate:bg-neutral-600 dark:focus-visible:outline-neutral-200 dark:disabled:border-neutral-500 dark:disabled:bg-neutral-400 dark:disabled:checked:bg-neutral-500 forced-colors:appearance-auto;
}

[type="checkbox"]:checked {
  @apply text-white dark:text-neutral-800;
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}

[type="checkbox"]:indeterminate {
  @apply border-neutral-300 bg-neutral-800 text-white dark:border-neutral-600 dark:!bg-neutral-700 dark:text-neutral-800;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3e%3cg fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' %3e%3cline x1='10.75' y1='6' x2='1.25' y2='6'%3e%3c/line%3e%3c/g%3e%3c/svg%3e");
  background-size: 75% 75%;
  background-position: center;
  background-repeat: no-repeat;
}

[type="checkbox"]:disabled {
  @apply cursor-not-allowed border-neutral-300 bg-neutral-300 text-neutral-400 opacity-75 hover:text-neutral-300 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:text-neutral-500;
}

[type="checkbox"]:disabled:checked {
  @apply border-neutral-300 dark:border-neutral-600 dark:bg-neutral-600;
}

[type="checkbox"]:indeterminate:hover {
  @apply bg-neutral-800 dark:border-neutral-600 dark:!bg-neutral-700;
}

[type="radio"] {
  @apply size-4 cursor-pointer appearance-none rounded-full border border-neutral-300 bg-white checked:border-neutral-700 checked:bg-neutral-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:border-neutral-300 disabled:bg-neutral-100 disabled:checked:bg-neutral-100 dark:border-white/20 dark:bg-neutral-800 dark:checked:border-white/20 dark:checked:bg-neutral-900 dark:focus-visible:outline-neutral-200 dark:disabled:border-neutral-500 dark:disabled:bg-neutral-400 dark:disabled:checked:bg-neutral-500 forced-colors:appearance-auto;
}

[type="radio"]:checked {
  @apply text-white dark:text-neutral-800;
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}

[type="radio"]:disabled {
  @apply cursor-not-allowed border-neutral-300 bg-neutral-300 text-neutral-400 opacity-75 hover:text-neutral-300 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:text-neutral-500;
}

[type="radio"]:disabled:checked {
  @apply border-neutral-300 dark:border-neutral-600 dark:bg-neutral-600;
}

/* Datalist styling */
input[list] {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

/* Replace default datalist arrow in WebKit browsers */
input[list].replace-default-datalist-arrow::-webkit-calendar-picker-indicator {
  display: none !important;
  -webkit-appearance: none !important;
}

input[list].replace-default-datalist-arrow {
  padding-right: 2.5rem;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23737373' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
  background-position: right 0.75rem center;
  background-repeat: no-repeat;
  background-size: 1.25em 1.25em;
}

/* Dark mode datalist arrow */
.dark {
  input[list].replace-default-datalist-arrow {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23A1A1AA' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
  }
}

Examples

Basic horizontal stepper

A comprehensive multi-step form with horizontal progress indicators. Includes form validation, data persistence across steps, and the ability to navigate back to edit previous information.

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>๐ŸŽ‰ Hello, Code Detective! ๐Ÿ•ต๏ธ</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! ๐Ÿš€</small></p>
    </div>

    <footer>
      <p>Built with โค๏ธ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("๐ŸŽŠ Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! ๐Ÿ’ป");
  </script>
</body>
</html>

Vertical stepper

A vertical layout stepper ideal for forms with detailed step descriptions.

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>๐ŸŽ‰ Hello, Code Detective! ๐Ÿ•ต๏ธ</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! ๐Ÿš€</small></p>
    </div>

    <footer>
      <p>Built with โค๏ธ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("๐ŸŽŠ Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! ๐Ÿ’ป");
  </script>
</body>
</html>

Compact progress bar stepper

A minimalist stepper with a clean progress bar design.

Getting Started Step 1 of 3

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>๐ŸŽ‰ Hello, Code Detective! ๐Ÿ•ต๏ธ</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! ๐Ÿš€</small></p>
    </div>

    <footer>
      <p>Built with โค๏ธ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("๐ŸŽŠ Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! ๐Ÿ’ป");
  </script>
</body>
</html>

Configuration

The stepper component is powered by a Stimulus controller that provides form validation, progress persistence, keyboard navigation, and flexible styling configuration.

Controller Setup

Basic stepper structure with required data attributes:

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>๐ŸŽ‰ Hello, Code Detective! ๐Ÿ•ต๏ธ</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! ๐Ÿš€</small></p>
    </div>

    <footer>
      <p>Built with โค๏ธ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("๐ŸŽŠ Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! ๐Ÿ’ป");
  </script>
</body>
</html>

Configuration Values

Prop Description Type Default
currentStep
The initially active step index (0-based) Number 0
validateOnNext
Whether to validate required fields before advancing to the next step Boolean true
allowSkip
Allow jumping to any step by clicking indicators (bypasses visited-only restriction) Boolean false
scrollOnChange
Scroll to stepper top when changing steps Boolean false
saveProgress
Save form data to sessionStorage for persistence across page reloads Boolean true
storageKey
sessionStorage key for saving progress data String "stepper-progress"
indicatorBaseClass
Base CSS classes applied to all step indicators String ""
indicatorCompleteClass
CSS classes for completed step indicators String "bg-neutral-900..."
indicatorCurrentClass
CSS classes for the current active step indicator String "bg-neutral-900..."
indicatorVisitedClass
CSS classes for visited (but not current) step indicators String "bg-neutral-100..."
indicatorUpcomingClass
CSS classes for upcoming (not yet visited) step indicators String "bg-neutral-100..."
lineBaseClass
Base CSS classes applied to all connecting lines between indicators String ""
lineCompleteClass
CSS classes for completed connecting lines String "bg-neutral-900..."
lineIncompleteClass
CSS classes for incomplete connecting lines String "bg-neutral-200..."

Targets

Target Description Required
step
The content container for each step. Only the current step is visible. Required
indicator
The clickable indicator buttons/circles that show step progress and allow navigation Required
line
The connecting lines between step indicators (shows completion progress) Optional
prevButton
Button to navigate to the previous step (automatically hidden on first step) Optional
nextButton
Button to advance to the next step (automatically hidden on last step) Optional
submitButton
Button to submit the form (automatically shown only on the last step) Optional

Actions

Action Description Usage
next
Advance to the next step (with optional validation) data-action="click->stepper#next"
previous
Go back to the previous step data-action="click->stepper#previous"
goToStep
Jump to a specific step (requires data-step attribute on the indicator) data-action="click->stepper#goToStep" data-step="2"
submit
Submit the form after validating the final step data-action="click->stepper#submit"
reset
Reset the stepper to the first step and clear saved progress data-action="click->stepper#reset"

Events

stepper:change

Dispatched when the active step changes. Contains step index and status information.

document.addEventListener('stepper:change', (event) => {
  console.log('Current step:', event.detail.step);
  console.log('Total steps:', event.detail.totalSteps);
  console.log('Is first step:', event.detail.isFirst);
  console.log('Is last step:', event.detail.isLast);
});

stepper:validate

Dispatched during step validation. You can prevent default to add custom validation logic.

document.addEventListener('stepper:validate', (event) => {
  console.log('Validating step:', event.detail.step);
  // Add custom validation
  if (myCustomValidation() === false) {
    event.preventDefault(); // Prevent moving to next step
  }
});

stepper:save

Dispatched when step data is saved to sessionStorage. Contains the saved form data.

document.addEventListener('stepper:save', (event) => {
  console.log('Saved step:', event.detail.step);
  console.log('Form data:', event.detail.data);
});

stepper:submit

Dispatched when the form is submitted. You can prevent default to handle submission manually.

document.addEventListener('stepper:submit', (event) => {
  console.log('Form data:', event.detail.data);
  // Prevent default form submission
  event.preventDefault();
  // Handle submission manually (e.g., via Turbo Streams or custom fetch)
});

Data Attributes for Review Sections

Use these special data attributes to create dynamic review/summary sections that automatically populate with form data:

Attribute Description Usage
data-review-field
Automatically displays the value of a form field by its name attribute <span data-review-field="email"></span>
data-stepper-progress-bar
Automatically updates width to show completion percentage <div data-stepper-progress-bar></div>
data-stepper-step-counter
Displays current step number (e.g., 'Step 2 of 4') <span data-stepper-step-counter></span>
data-stepper-step-label
Displays the label for the current step from a comma-separated list <span data-stepper-step-label data-step-labels="Personal,Contact,Review"></span>

Accessibility Features

  • Keyboard Navigation: Press Enter on form inputs to advance to the next step (works on all inputs except textareas and buttons)
  • Form Validation: Automatic HTML5 validation with reportValidity() for required fields
  • ARIA Support: Step indicators include aria-current="step" for the active step
  • Progress Persistence: Form data is automatically saved to sessionStorage and restored on page reload
  • Focus Management: Proper focus handling when navigating between steps

Advanced Features

  • Custom Validation: Hook into the stepper:validate event to add custom validation logic beyond HTML5 validation
  • Dynamic Review Sections: Use data-review-field attributes to automatically populate review/summary steps with form data
  • Flexible Navigation: Users can navigate back to previous steps or jump to any visited step (or any step if allowSkip is enabled)
  • Progress Tracking: Visual indicators show completed, current, visited, and upcoming steps with customizable styling
  • Customizable Styling: Override default CSS classes for indicators and lines to match your design system
  • Form Integration: Works seamlessly with standard HTML forms and can trigger form submission on the final step

Table of contents

Get notified when new components come out