A TypeScript multi-step form library that works with any existing form. Zero dependencies, framework-agnostic, and designed to be completely non-intrusive.
- π― 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
npm install @lucaismyname/stepper-js
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'
}
});
<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>
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.
<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>
- 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
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()
- 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
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
}
// 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
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 */
StepperJS includes a powerful animation system that uses opacity and pointer-events instead of display: none
, enabling smooth GPU-accelerated transitions.
// 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'
}
- 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
andopacity
properties - Accessibility: Steps remain in DOM for screen readers
// 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 }
});
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)'
StepperJS offers a comprehensive validation system with multiple layers:
-
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()
andfield.reportValidity()
to utilize native validation UI
- When
-
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
-
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
- After form submission, you can display server-side validation errors with
-
Validation Utilities
- The library includes helper functions for common validation scenarios:
isValidEmail()
- Email format validationisValidPhone()
- Basic phone number validation- Various validation rules in the
validationRules
object
- The library includes helper functions for common validation scenarios:
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;
}
});
// 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);
}
});
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');
}
});
// 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);
// 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);
});
/* 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;
}
// 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);
}
});
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);
});
- Always check selectors exist before initializing
- Use specific selectors to avoid conflicts
- Test with different form structures
- Handle errors gracefully with try/catch
- Use
initializeWhenReady()
for third-party forms - Add debug logging during development
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
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:
- http://localhost:3000/examples/basic.html
- http://localhost:3000/examples/cdn-usage.html
- http://localhost:3000/examples/error-handling-demo.html
- http://localhost:3000/examples/fundraisingbox-example.html
StepperJS is designed to work seamlessly with forms loaded by third-party scripts (like FundraisingBox, Typeform, etc.).
StepperJS provides several methods for handling dynamically loaded forms:
initializeWhenReady()
- Basic polling for form availabilityinitializeOnDOMReady()
- Waits for DOMContentLoaded event before initializinginitializeWithObserver()
- 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!');
}
});
<!-- 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>
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']
});
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);
}
});
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)
# 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)
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 }
]
};
MIT Β© Luca Mack