Skip to content

LucaIsMyName/stepper.js

Repository files navigation

StepperJS πŸš€

A TypeScript multi-step form library that works with any existing form. Zero dependencies, framework-agnostic, and designed to be completely non-intrusive.

npm version TypeScript License: MIT

✨ Features

  • 🎯 Drop-in compatibility - Works with any existing form without modifications
  • πŸ“± Framework agnostic - Vanilla JS/TS, works with React, Vue, Angular, etc.
  • πŸ”§ TypeScript-first - Full type safety and IntelliSense support
  • πŸͺΆ Lightweight - <7KB gzipped, zero dependencies
  • 🎨 Unstyled - Bring your own CSS, works with any design system
  • ✨ Smooth animations - Built-in step transitions (fade, slide horizontal/vertical)
  • βœ… Validation ready - HTML5 validation + custom validation functions + server-side error handling
  • πŸ”„ Server-side errors - Handle and display backend validation errors
  • β™Ώ Accessible - ARIA attributes and keyboard navigation
  • πŸ“Š Progress tracking - Built-in step completion tracking

πŸš€ Quick Start

Installation

npm install @lucaismyname/stepper-js

Basic Usage

import { MultiStepForm } from '@lucaismyname/stepper-js';

const stepper = new MultiStepForm({
  formSelector: '#my-form',
  steps: [
    { selector: '.step-1', validate: true },
    { selector: '.step-2', validate: true },
    { selector: '.step-3', validate: false }
  ],
  buttonLabels: {
    next: 'Continue',
    prev: 'Go Back',
    submit: 'Complete'
  },
  animation: {
    type: 'slideHorizontal', // 'fade', 'slideHorizontal', 'slideHorizontalReverse', 'slideVertical', 'none'
    duration: 300,
    easing: 'ease-in-out'
  }
});

HTML Structure

<form id="my-form">
  <div class="step-1">
    <h2>Personal Info</h2>
    <input type="text" name="firstName" required>
    <input type="email" name="email" required>
  </div>
  
  <div class="step-2">
    <h2>Address</h2>
    <input type="text" name="street" required>
    <input type="text" name="city" required>
  </div>
  
  <div class="step-3">
    <h2>Confirmation</h2>
    <p>Please review your information...</p>
  </div>
</form>

βš™οΈ Form Requirements & Compatibility

βœ… What Forms Work

StepperJS works with any HTML form that has:

  • A <form> element (can be selected by any CSS selector)
  • Container elements for each step (divs, fieldsets, sections, etc.)
  • Standard form fields (input, select, textarea)

The library is designed to be non-intrusive - it doesn't modify your form structure, only adds navigation functionality and validation handling.

πŸ—οΈ Required Structure

<form id="any-form">
  <!-- Step 1: Any container element -->
  <div class="step-1">
    <input name="field1" required>
    <input name="field2">
  </div>
  
  <!-- Step 2: Can be any element type -->
  <fieldset id="step-2">
    <select name="field3" required></select>
    <textarea name="field4"></textarea>
  </fieldset>
  
  <!-- Step 3: Nested elements work too -->
  <section data-step="final">
    <div class="form-group">
      <input type="checkbox" name="terms" required>
    </div>
  </section>
</form>

🚫 What Doesn't Work

  • No form element: Must have a <form> tag
  • Invalid selectors: CSS selectors that don't exist
  • Empty steps: Step containers with no form fields
  • Duplicate selectors: Multiple elements matching the same selector

πŸ›‘οΈ Error Handling

The library provides comprehensive error handling:

try {
  const stepper = new MultiStepForm({
    formSelector: '#nonexistent-form', // ❌ Will throw error
    steps: [
      { selector: '.missing-step' },    // ❌ Will be filtered out
      { selector: '#valid-step' }       // βœ… Will work
    ]
  });
} catch (error) {
  console.error('Initialization failed:', error.message);
  // "Form not found: #nonexistent-form"
}

Error Types:

  • Form not found: Invalid formSelector
  • No valid steps: All step selectors are invalid
  • Timeout errors: When using initializeWhenReady()

πŸ“Š Step Limitations

  • Minimum steps: 1 (at least one valid step required)
  • Maximum steps: Unlimited! Create as many as needed
  • Step validation: Each step is validated independently
  • Navigation: Automatically generated for any number of steps

πŸ“– API Reference

Configuration Options

interface MultiStepConfig {
  formSelector: string;              // CSS selector for form
  steps: StepConfig[];               // Array of step configurations
  buttonLabels?: ButtonLabels;       // Global button text overrides
  validation?: ValidationFunction;   // Custom validation function
  onStepChange?: StepChangeCallback; // Step change event handler
  onValidationError?: ErrorCallback; // Validation error handler
  beforeStepChange?: BeforeChangeCallback; // Pre-step-change hook
  autoScroll?: boolean;              // Auto-scroll on step change (default: true)
  scrollOffset?: number;             // Scroll offset in pixels (default: 20)
  buttonClasses?: ButtonClasses;     // Custom CSS classes for buttons
  animation?: AnimationConfig;       // Animation configuration for step transitions
}

interface AnimationConfig {
  type: AnimationType;               // Animation type
  duration?: number;                 // Duration in milliseconds (default: 300)
  easing?: string;                   // CSS easing function (default: 'ease-in-out')
}

type AnimationType = 'none' | 'fade' | 'slideHorizontal' | 'slideHorizontalReverse' | 'slideVertical';

interface StepConfig {
  selector: string;                  // CSS selector for step container
  validate?: boolean;                // Validate step on change (default: true)
  prevButton?: string;               // Custom text for previous button on this step
  nextButton?: string;               // Custom text for next button on this step
}

interface ButtonLabels {
  next?: string;                     // Next button text
  prev?: string;                     // Previous button text
  submit?: string;                   // Submit button text
}

interface ValidationFunction {
  (fields: NodeListOf<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>): boolean; // Custom validation function
}

interface StepChangeCallback {
  (current: number, previous: number): void; // Step change callback
}

interface ErrorCallback {
  (errors: [
    {
      step: number;                      // Step index (0-based)
      field: string;                     // Field name
      message: string;                   // Error message
    }
  ]): void; // Error callback
}

interface BeforeChangeCallback {
  (current: number, next: number): boolean; // Pre-step-change callback
}

interface ButtonClasses {
  prev?: string;                     // Previous button class
  next?: string;                     // Next button class
  submit?: string;                   // Submit button class
}

Public Methods

// Navigation
stepper.goToStep(2);                    // Go to specific step
stepper.nextStep();                     // Go to next step
stepper.prevStep();                     // Go to previous step

// Validation
stepper.validateCurrentStep();          // Validate current step
stepper.isStepValid(1);                // Check if step is valid
stepper.showServerErrors(errors);       // Display server errors
stepper.clearErrors();                  // Clear all errors

// Data
stepper.getFormData();                  // Get FormData object
stepper.getFormDataObject();            // Get plain object

// State
stepper.getCurrentStep();               // Get current step index
stepper.getTotalSteps();               // Get total number of steps

// Lifecycle
stepper.destroy();                      // Clean up and destroy instance

🎨 Styling

StepperJS is completely unstyled by default. It adds these CSS classes that you can style:

/* Navigation */
.stepper-nav          /* Navigation container */
.stepper-prev         /* Previous button */
.stepper-next         /* Next button */
.stepper-submit       /* Submit button */

/* Validation */
.stepper-error-message /* Field error message */
.stepper-step-error   /* Step-level error message */

/* Animation (automatically added) */
.stepper-step-container /* Step container with animation support */
.stepper-step-active    /* Currently visible step */
.stepper-step-inactive  /* Hidden step */
.stepper-animating      /* Step during animation */

πŸ”§ Advanced Usage

Step Animations

StepperJS includes a powerful animation system that uses opacity and pointer-events instead of display: none, enabling smooth GPU-accelerated transitions.

Animation Types

// Fade (default) - Simple opacity transition
animation: {
  type: 'fade',
  duration: 300,
  easing: 'ease-in-out'
}

// Slide Horizontal - Slides left-to-right when moving forward
animation: {
  type: 'slideHorizontal',
  duration: 400,
  easing: 'ease-in-out'
}

// Slide Horizontal Reverse - Slides right-to-left when moving forward
animation: {
  type: 'slideHorizontalReverse',
  duration: 400,
  easing: 'ease-in-out'
}

// Slide Vertical - Slides top-to-bottom
animation: {
  type: 'slideVertical',
  duration: 400,
  easing: 'ease-in-out'
}

// No Animation - Instant step changes
animation: {
  type: 'none'
}

How It Works

  • Active steps: opacity: 1, pointer-events: auto, position: relative
  • Inactive steps: opacity: 0, pointer-events: none, position: absolute
  • Direction-aware: Animations automatically reverse when going backward
  • Performance: Uses GPU-accelerated transform and opacity properties
  • Accessibility: Steps remain in DOM for screen readers

Dynamic Animation Changes

// Initialize with one animation
let stepper = new MultiStepForm({
  formSelector: '#form',
  steps: [/* ... */],
  animation: { type: 'fade' }
});

// Switch to different animation
stepper.destroy();
stepper = new MultiStepForm({
  formSelector: '#form',
  steps: [/* ... */],
  animation: { type: 'slideHorizontal', duration: 500 }
});

Custom Easing Functions

animation: {
  type: 'slideHorizontal',
  duration: 600,
  easing: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)' // Bounce effect
}

// Other examples:
// 'linear'
// 'ease'
// 'ease-in'
// 'ease-out'
// 'ease-in-out'
// 'cubic-bezier(0.4, 0, 0.2, 1)'

Validation System

StepperJS offers a comprehensive validation system with multiple layers:

  1. HTML5 Validation (Default)

    • When validate: true is set for a step (default), the library uses the browser's native HTML5 validation
    • This includes checking attributes like required, pattern, min, max, minlength, maxlength, etc.
    • The library calls field.checkValidity() and field.reportValidity() to utilize native validation UI
  2. Custom Validation Functions

    • You can provide a custom validation function to implement more complex validation logic
    • This function receives all form fields in the current step and must return a boolean
    • Custom validation overrides the default HTML5 validation
  3. Server-Side Error Handling

    • After form submission, you can display server-side validation errors with showServerErrors()
    • This maps errors to specific fields and steps, and automatically navigates to the first step with errors
  4. Validation Utilities

    • The library includes helper functions for common validation scenarios:
      • isValidEmail() - Email format validation
      • isValidPhone() - Basic phone number validation
      • Various validation rules in the validationRules object

Custom Validation

const stepper = new MultiStepForm({
  formSelector: '#form',
  steps: [{ selector: '.step-1' }],
  validation: (fields) => {
    // Custom validation logic
    for (const field of fields) {
      if (field.name === 'email' && !isValidEmail(field.value)) {
        return false;
      }
    }
    return true;
  }
});

Server-Side Validation

// Handle form submission
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  
  try {
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: stepper.getFormData()
    });
    
    if (!response.ok) {
      const errors = await response.json();
      stepper.showServerErrors(errors.validationErrors);
    }
  } catch (error) {
    console.error('Submission failed:', error);
  }
});

Event Handling

const stepper = new MultiStepForm({
  formSelector: '#form',
  steps: [/* ... */],
  onStepChange: (current, previous) => {
    console.log(`Step changed: ${previous} β†’ ${current}`);
    updateProgressBar(current);
  },
  beforeStepChange: async (current, next) => {
    // Async validation or data fetching
    if (next === 2) {
      const isValid = await validateWithServer();
      return isValid; // Return false to prevent step change
    }
    return true;
  },
  onValidationError: (errors) => {
    // Custom error handling
    showNotification('Please fix the errors before continuing');
  }
});

πŸ”§ Troubleshooting

Common Issues & Solutions

❌ "Form not found" Error

// Problem: Invalid form selector
const stepper = new MultiStepForm({
  formSelector: '#wrong-id' // Form doesn't exist
});

// Solution: Check your form selector
const form = document.querySelector('#your-form-id');
console.log('Form exists:', !!form);

❌ "No valid steps found" Error

// Problem: All step selectors are invalid
const stepper = new MultiStepForm({
  formSelector: '#my-form',
  steps: [
    { selector: '.nonexistent' },  // Doesn't exist
    { selector: '#missing' }       // Doesn't exist
  ]
});

// Solution: Verify step selectors exist
const steps = ['.step-1', '.step-2', '.step-3'];
steps.forEach(selector => {
  const element = document.querySelector(selector);
  console.log(`${selector} exists:`, !!element);
});

❌ Steps Not Hiding/Showing

/* Problem: CSS conflicts with display property */
.my-step {
  display: block !important; /* Overrides stepper */
}

/* Solution: Use more specific CSS or avoid !important */
.stepper-hidden {
  display: none !important;
}

❌ Third-Party Form Not Loading

// Problem: Form loads after timeout
const stepper = await initializeWhenReady(config, {
  timeout: 1000 // Too short
});

// Solution: Increase timeout and add debugging
const stepper = await initializeWhenReady(config, {
  timeout: 10000,
  onTimeout: () => {
    console.log('Form still not found after 10 seconds');
    // Check if form exists manually
    const form = document.querySelector('#fbPaymentForm');
    console.log('Form exists now:', !!form);
  }
});

πŸ› Debug Mode

Enable detailed logging:

const stepper = new MultiStepForm({
  formSelector: '#my-form',
  steps: [/* your steps */],
  onStepChange: (current, previous) => {
    console.log(`Debug: Step ${previous} β†’ ${current}`);
  },
  beforeStepChange: (current, next) => {
    console.log(`Debug: Validating step ${current} before going to ${next}`);
    return true;
  },
  onValidationError: (errors) => {
    console.log('Debug: Validation errors:', errors);
  }
});

// Check step elements
stepper.steps?.forEach((step, index) => {
  console.log(`Step ${index}:`, step.element, 'Validate:', step.validate);
});

βœ… Best Practices

  1. Always check selectors exist before initializing
  2. Use specific selectors to avoid conflicts
  3. Test with different form structures
  4. Handle errors gracefully with try/catch
  5. Use initializeWhenReady() for third-party forms
  6. Add debug logging during development

🌟 Examples

Check out the /examples directory for complete working examples:

  • Basic Example - Simple 3-step form with validation
  • FundraisingBox Example - Real-world integration with third-party donation forms
  • CDN Usage - Using StepperJS via CDN with dynamic forms
  • Error Handling Demo - Server-side error handling examples

Running Examples Locally

The project includes a simple server script to run the examples:

# Using the included server script
node serve-examples.js

# Or using Vite for development
npx vite examples --host

The server will start at http://localhost:3000/ with the following examples available:

πŸ”„ Third-Party Form Integration

StepperJS is designed to work seamlessly with forms loaded by third-party scripts (like FundraisingBox, Typeform, etc.).

Dynamic Form Loading

StepperJS provides several methods for handling dynamically loaded forms:

  1. initializeWhenReady() - Basic polling for form availability
  2. initializeOnDOMReady() - Waits for DOMContentLoaded event before initializing
  3. initializeWithObserver() - Uses MutationObserver for better performance

All these methods accept the same configuration options:

interface DynamicLoaderOptions {
  timeout?: number;           // Maximum time to wait (default: 10000ms)
  pollInterval?: number;      // Polling interval (default: 100ms)
  onInitialized?: Function;   // Callback when initialized
  onTimeout?: Function;       // Callback when timeout reached
  waitForSelectors?: string[]; // Additional selectors to wait for
}
import { initializeWhenReady } from '@lucaismyname/stepper-js';

// Wait for third-party form to load
const stepper = await initializeWhenReady({
  formSelector: '#third-party-form',
  steps: [
    { selector: '#step-1' },
    { selector: '#step-2' }
  ]
}, {
  timeout: 10000,
  waitForSelectors: ['#important-field'], // Wait for specific elements
  onInitialized: (stepper) => {
    console.log('Form ready!');
  }
});

CDN Usage (No Build Step)

<!-- Include via CDN -->
<script src="https://unpkg.com/@lucaismyname/stepper-js@latest/dist/stepper.umd.js"></script>

<script>
  const { MultiStepForm, initializeWhenReady } = window.StepperJS;
  
  // Use after third-party form loads
  initializeWhenReady({
    formSelector: '#dynamic-form',
    steps: [
      { selector: '#step-1' },
      { selector: '#step-2' }
    ]
  });
</script>

FundraisingBox Integration

Perfect for NGO donation forms:

// Configuration for FundraisingBox forms
const stepper = await initializeWhenReady({
  formSelector: '#fbPaymentForm',
  steps: [
    { selector: '#amountBox', validate: true },
    { selector: '#donorData', validate: true },
    { selector: '#paymentMethodBox', validate: true }
  ],
  validation: (fields) => {
    // Custom validation for donation amounts, email, etc.
    return validateDonationForm(fields);
  }
}, {
  timeout: 5000,
  waitForSelectors: ['#payment_amount', '#payment_email']
});

Handling Complex Third-Party Logic

const stepper = new MultiStepForm({
  formSelector: '#complex-form',
  steps: [/* steps */],
  beforeStepChange: async (current, next) => {
    // Handle third-party dependencies
    if (next === 2) {
      await loadPaymentMethods();
      updateFormFields();
    }
    return true;
  },
  onStepChange: (current, previous) => {
    // Trigger third-party events
    triggerThirdPartyValidation(current);
  }
});

πŸ“Ž Project Structure

src/
β”œβ”€β”€ MultiStepForm.ts   # Main class implementation
β”œβ”€β”€ DynamicLoader.ts   # Dynamic form loading utilities
β”œβ”€β”€ validation.ts     # Validation utilities
β”œβ”€β”€ utils.ts          # Helper functions
β”œβ”€β”€ types.ts          # TypeScript interfaces
└── index.ts          # Entry point and exports

examples/            # Example implementations
dist/                # Built files (after build)

πŸ’Ώ Build Commands

# Install dependencies
npm install

# Development mode
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview

# Run examples server
node serve-examples.js
npm run serve-examples

The build process creates multiple output formats:

  • ESM: dist/stepper.es.js
  • CommonJS: dist/stepper.cjs.js
  • UMD: dist/stepper.umd.js (for direct browser usage)
  • TypeScript declarations: dist/*.d.ts (full type definitions for all exports)
  • TypeScript source files: dist/src/*.ts (original TypeScript source code)

TypeScript Support

This library is built with TypeScript and provides comprehensive type definitions:

// Import with full TypeScript support
import { MultiStepForm, ValidationError } from '@lucaismyname/stepper-js';

// All interfaces and types are exported
import type { MultiStepConfig, StepConfig } from '@lucaismyname/stepper-js';

// Example with type safety
const config: MultiStepConfig = {
  formSelector: '#my-form',
  steps: [
    { selector: '.step-1', validate: true }
  ]
};

πŸ“ License

MIT Β© Luca Mack

πŸ”— Links

About

A drop-in Multi-Step-Form Library

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published