A lightweight wizard UI component for accessible multi-step flows in vanilla JavaScript.
- Supports accessibility (ARIA-compliant).
- Fully customizable wizard steps and navigation.
- Works with both ES modules and CommonJS.
- Built-in form validation and control.
- Async step guards with
before_step_change. - Conditional required fields using
data-require-ifattribute. - Dynamic required fields with
on-active-requiredclass. - Customizable validation highlighting.
- Quick Start - copy-paste CDN example.
- Installation - npm, pnpm, bun and CDN links.
- Usage - module imports, HTML structure and wizard controls.
- Form Validation - required fields, radio groups and conditional rules.
- Config Wizard - options, async guards and i18n.
- Events Management - lifecycle, navigation, validation and submit events.
- Test Folder - package consumption smoke tests.
- CDN Releases - how to publish tags that work on jsDelivr.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/AdrianVillamayor/Wizard-JS@2.1.1/dist/main.min.css">
<form class="wizard" method="POST">
<aside class="wizard-content">
<div class="wizard-step" data-wz-title="Account">
<input type="email" name="email" class="required" placeholder="Email">
</div>
<div class="wizard-step" data-wz-title="Plan">
<label><input type="radio" name="plan" value="free" required> Free</label>
<label><input type="radio" name="plan" value="pro" required> Pro</label>
</div>
</aside>
</form>
<script src="https://cdn.jsdelivr.net/gh/AdrianVillamayor/Wizard-JS@2.1.1/dist/index.js"></script>
<script>
const wizard = new Wizard({
wz_class: ".wizard"
});
wizard.init();
</script>You can install the package from npm:
npm install @adrii_/wizard-jspnpm add @adrii_/wizard-jsbun install @adrii_/wizard-jsFor CDN usage:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/AdrianVillamayor/Wizard-JS@latest/dist/main.min.css">
<script src="https://cdn.jsdelivr.net/gh/AdrianVillamayor/Wizard-JS@latest/dist/index.js"></script>For production usage, it is better to pin a specific version instead of @latest:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/AdrianVillamayor/Wizard-JS@2.1.1/dist/main.min.css">
<script src="https://cdn.jsdelivr.net/gh/AdrianVillamayor/Wizard-JS@2.1.1/dist/index.js"></script>The GitHub CDN uses files from the release tag, so every published tag must include the generated dist/ folder.
const Wizard = require('@adrii_/wizard-js');
const myWizard = new Wizard({
/* your configuration here */
});
myWizard.init();import Wizard from '@adrii_/wizard-js';
const myWizard = new Wizard({
/* your configuration here */
});
myWizard.init();For content-only wizards:
<div class="wizard">
<aside class="wizard-content container">
<div class="wizard-step">
<!-- Step content -->
</div>
<!-- More steps -->
</aside>
</div>For wizards with form purpose, it manages the required fields and validates them:
<form class="wizard" method="POST">
<aside class="wizard-content container">
<div class="wizard-step">
<input type="text" name="name" class="required" placeholder="Enter a short campaign name">
</div>
<!-- More steps -->
</aside>
</form>To launch the wizard:
let args = {
"wz_class": ".wizard",
"wz_nav_style": "dots",
"wz_button_style": ".btn .btn-sm .mx-3",
"wz_ori": "vertical",
"buttons": true,
"navigation": 'buttons',
"finish": "Save!"
};
const wizard = new Wizard(args);
wizard.init();To restart the wizard:
wizard.reset();To update the wizard, deleting or adding steps:
wizard.update();To lock the wizard at a specific step:
wizard.lock();To unlock the wizard:
wizard.unlock();Wizard-JS includes built-in form validation for required fields.
To mark an input as required, add the required attribute or the required class:
<input type="text" name="email" required>
<!-- or -->
<input type="text" name="email" class="required">Radio buttons are validated by their shared name. One checked option is enough to continue:
<label>
<input type="radio" name="plan" value="free" required>
Free
</label>
<label>
<input type="radio" name="plan" value="pro" required>
Pro
</label>You can also use the required class instead of the native attribute:
<input type="radio" name="answer" value="yes" class="required">
<input type="radio" name="answer" value="no" class="required">You can make fields conditionally required based on the value of another field using the data-require-if attribute.
The format is data-require-if="fieldID:requiredValue".
<input type="checkbox" id="subscribe" name="subscribe">
<input type="email" name="email" data-require-if="subscribe:true" placeholder="Enter your email">In this example, the email field becomes required if the checkbox with id subscribe is checked (true).
Use the on-active-required class to mark fields that become required only when the step is active.
<input type="text" name="username" class="on-active-required" placeholder="Enter your username">You can customize the validation highlighting behavior:
highlight: Set totrueto enable highlighting invalid fields.highlight_time: Duration (in milliseconds) to display the highlight. Default is1000.highlight_type: An object to define the classes for different highlight types.
Example:
let args = {
/* Other configurations */
"highlight": true,
"highlight_time": 1500,
"highlight_type": { error: "error", warning: "warning", success: "success", info: "info" }
};Set the step title for the navigation. If left blank, the system will automatically add Step + step number as the title.
<div class="wizard-step" data-wz-title="Configuration">
<!-- Step content -->
</div>Define conditional required fields based on another field's value.
<input type="email" name="email" data-require-if="subscribe:true" placeholder="Enter your email">Options allowing you to modify the behavior and actions:
| Parameter | Type | Default | Definition / Value |
|---|---|---|---|
wz_class |
String | .wizard |
Wizard main container target |
wz_ori |
String | .horizontal |
Wizard orientation (.horizontal, .vertical) |
wz_nav |
String | .wizard-nav |
Navigation container class |
wz_nav_style |
String | dots |
Style of navigation steps (dots, tabs, progress) |
wz_content |
String | .wizard-content |
Body container class |
wz_buttons |
String | .wizard-buttons |
Action button container class |
wz_button |
String | .wizard-btn |
Class of Previous, Next, and Finish action buttons |
wz_button_style |
String | .btn |
Basic button style |
wz_step |
String | .wizard-step |
Class for both nav and body steps |
wz_form |
String | .wizard-form |
Class of the form that contains the wizard |
wz_next |
String | .next |
Class of Next action button |
wz_prev |
String | .prev |
Class of Prev action button |
wz_finish |
String | .finish |
Class of Finish action button |
wz_highlight |
String | .highlight-error |
Class for highlights when validation errors occur |
current_step |
Number | 0 |
Active wizard step |
steps |
Number | 0 |
Number of wizard steps |
highlight_time |
Number | 1000 |
Display time for validation highlight (in milliseconds) |
navigation |
String | all |
Navigation mode (buttons, nav, all) |
buttons |
Boolean | true |
Show or hide the action buttons |
nav |
Boolean | true |
Show or hide the header navigation |
highlight |
Boolean | true |
Enable or disable field highlighting on validation errors |
next |
String | Next |
Text for the Next button |
prev |
String | Prev |
Text for the Prev button |
finish |
String | Submit |
Text for the Finish button |
before_step_change |
Function | null |
Hook executed before moving forward. Can return false or a rejected promise to block the step change |
bubbles |
Boolean | true |
Enable or disable event bubbling for custom events |
highlight_type |
Object | { error: "error", warning: "warning", success: "success", info: "info" } |
Classes for different validation highlight effects |
i18n |
Object | Various | Internationalization messages for errors, titles, and warnings |
Use before_step_change when you need to perform async work before allowing the wizard to move to the next step.
const wizard = new Wizard({
wz_class: ".wizard",
before_step_change: async ({ currentStepElement, isAsyncStep }) => {
if (!isAsyncStep) {
return true;
}
const stepId = currentStepElement?.getAttribute("data-id");
if (stepId === "campaign") {
const response = await fetch("/api/check-campaign", {
method: "POST"
});
return response.ok;
}
if (stepId === "notification") {
const title = currentStepElement
?.querySelector('input[name="title"]')
?.value;
return title === "LoR";
}
return true;
}
});To mark a step as async-only, add either data-wz-async="true" or data-async-step="true" to the current .wizard-step.
If you have more than one async step, prefer identifying each step with a stable attribute such as data-id instead of relying only on the numeric index.
<div class="wizard-step" data-id="campaign" data-wz-async="true"></div>
<div class="wizard-step" data-id="notification" data-wz-async="true"></div>The hook context exposes:
currentStepnextStepcurrentStepElementnextStepElementtriggerisAsyncStepwizard
While the hook is pending, the wizard root gets data-wz-pending="true" and extra forward clicks are ignored.
The i18n object allows you to define custom error messages for different wizard actions:
empty_wz: "No item has been found with which to generate the Wizard."empty_nav: "Nav does not exist or is empty."empty_content: "Content does not exist or is empty."empty_html: "Undefined or null content cannot be added."empty_update: "Nothing to update."no_nav: "Both the nav and the buttons are disabled, there is no navigation system."form_validation: "One or more of the form fields are invalid."diff_steps: "Discordance between the steps of nav and content."random: "There has been a problem, check the configuration and use of the wizard."already_defined: "This item is already defined."title: "Step" (used for default step titles if not specified in the HTML).
Wizard-JS provides several events to help you manage and respond to user interactions.
To identify when the wizard is fully generated and loaded.
document.addEventListener("wz.ready", function (e) {
console.log(`Target:`, e.detail.target); // .wizard
console.log(`DOM Element:`, e.detail.elem); // DOM element
});Triggered when the wizard is updated.
let wz_class = ".wizard";
let $wz_doc = document.querySelector(wz_class)$wz_doc.addEventListener("wz.update", function (e) {
console.log(`Target:`, e.detail.target);
console.log(`DOM Element:`, e.detail.elem);
});When the wizard is locked:
$wz_doc.addEventListener("wz.lock", function (e) {
alert("Wizard is locked");
});When the wizard is unlocked:
$wz_doc.addEventListener("wz.unlock", function (e) {
alert("Wizard is unlocked");
});Moving to the previous step:
$wz_doc.addEventListener("wz.btn.prev", function (e) {
alert("Previous Step");
});Moving to the next step:
$wz_doc.addEventListener("wz.btn.next", function (e) {
alert("Next Step");
});Navigating forward with the navbar:
$wz_doc.addEventListener("wz.nav.forward", function (e) {
alert("Forward Navigation");
});Navigating backward with the navbar:
$wz_doc.addEventListener("wz.nav.backward", function (e) {
alert("Backward Navigation");
});Triggered when there is an error validating the data of the active form step.
$wz_doc.addEventListener("wz.error", function (e) {
console.log(`ID:`, e.detail.id); // form_validation
console.log(`Message:`, e.detail.msg); // Form validation message
console.log(`Target:`, e.detail.target); // Array of invalid elements
});Triggered when before_step_change starts, finishes or throws an error.
$wz_doc.addEventListener("wz.pending", function (e) {
console.log("Pending step change", e.detail.currentStep, e.detail.nextStep);
});
$wz_doc.addEventListener("wz.pending.done", function (e) {
console.log("Step change allowed:", e.detail.allowed);
});
$wz_doc.addEventListener("wz.pending.error", function (e) {
console.error("Async step failed", e.detail.error);
});If it is a form, at the end it will fire the following event:
$wz_doc.addEventListener("wz.form.submit", function (e) {
alert("Form Submitted");
});If it is not a form, at the end it will fire the following event:
$wz_doc.addEventListener("wz.end", function (e) {
alert("Wizard is finished");
});When the wizard is restarted:
$wz_doc.addEventListener("wz.reset", function (e) {
alert("Wizard has restarted");
});The repository includes a test/ folder with smoke examples that show how to consume the package in different environments:
test/ts: TypeScript consumer using the packed tarball.test/js: JavaScript ESM consumer using the packed tarball.test/cjs: CommonJS consumer using the packed tarball.test/cdn: browser example using jsDelivr CDN.test/base.css: minimal visual base used only by the browser smoke examples.
The name test/ is intentional here: these are not unit tests, but package consumption smoke tests.
To run the automated smoke checks:
pnpm run smokeTo run them one by one:
pnpm run smoke:ts
pnpm run smoke:js
pnpm run smoke:cjsTo prepare the browser examples:
pnpm run browser:prepareTo open each browser example with Vite:
pnpm run browser:ts
pnpm run browser:js
pnpm run browser:cjs
pnpm run browser:cdnBrowser examples available in test/:
test/ts/index.html: TypeScript + package CSS.test/js/index.html: JavaScript ESM + package CSS.test/cjs/index.html: browser UMD/global test using the packaged UMD bundle installed in the fixture.test/cdn/index.html: CDN/browser test using jsDelivr.
Note about cjs in the browser:
test/cjs/index.jsvalidates CommonJS in Node.test/cjs/index.htmlvalidates the browser global/UMD bundle.
The CDN example can also be opened directly in a browser if you only want to test the published jsDelivr version.
Try it on CodePen:
The local repo demo also includes an async example with multiple async steps:
campaign: only advances whendropdown === "other"notification: only advances whentitle === "LoR"
You can run it with:
pnpm run devThis project uses the jsDelivr GitHub package:
https://www.jsdelivr.com/package/gh/AdrianVillamayor/Wizard-JS
Before creating a new release tag, run pnpm run build and commit the generated dist/ files. Without dist/, CDN URLs like /dist/index.js and /dist/main.min.css will not work for that tag.
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
