npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gooonzick/wizard-core

v1.3.0

Published

Core package for WizardForm framework

Readme

@gooonzick/wizard-core

A framework-agnostic, type-safe state machine for building multi-step wizards in TypeScript.

Features

  • Framework Agnostic - Use with React, Vue, Svelte, vanilla JS, or any TypeScript project
  • Type Safe - Full TypeScript generics support for your data types
  • Flexible Navigation - Static, conditional, or dynamic step transitions
  • Navigation History - Built-in history stack for reliable back navigation through conditional flows
  • Validation - Built-in validators, schema support, and custom predicates
  • Extensible - Context, guards, lifecycle hooks, and side effects
  • State Machine - Predictable behavior based on finite state machine principles
  • Zero Dependencies - Core library has no external dependencies (only for Standard Schema validation)

Installation

npm install @gooonzick/wizard-core

Quick Start

import { WizardMachine, createLinearWizard } from "@gooonzick/wizard-core";

// 1. Define your data type
type SignupData = {
  name: string;
  email: string;
};

// 2. Create a wizard
const wizard = createLinearWizard<SignupData>({
  id: "signup",
  steps: [
    {
      id: "name",
      title: "What's your name?",
      validate: (data) => ({
        valid: Boolean(data.name),
        errors: data.name ? undefined : { name: "Required" },
      }),
    },
    {
      id: "email",
      title: "What's your email?",
      validate: (data) => ({
        valid: data.email?.includes("@") ?? false,
        errors: data.email?.includes("@")
          ? undefined
          : { email: "Invalid email" },
      }),
      onSubmit: async (data) => {
        console.log("Form submitted:", data);
      },
    },
  ],
  onComplete: (data) => {
    console.log("Wizard complete:", data);
  },
});

// 3. Create a state machine
const machine = new WizardMachine(wizard, {}, { name: "", email: "" });

// 4. Navigate
console.log(machine.snapshot.currentStepId); // "name"
await machine.goNext();
console.log(machine.snapshot.currentStepId); // "email"

Core Concepts

Declarative Definitions

Define wizards as data, not imperative code:

import type { WizardDefinition } from "@gooonzick/wizard-core";

const wizard: WizardDefinition<MyData> = {
  id: "my-wizard",
  initialStepId: "step1",
  steps: {
    step1: {
      /* ... */
    },
    step2: {
      /* ... */
    },
  },
};

State Machine

All navigation and validation goes through WizardMachine:

const machine = new WizardMachine(definition, context, initialData, {
  onStateChange: (state) => {
    // React to state changes
  },
});

await machine.goNext();
await machine.validate();

Three Transition Types

Navigate between steps in different ways:

Static - Always go to the same step:

next: { type: "static", to: "next-step" }

Conditional - Branch based on data:

next: {
  type: "conditional",
  branches: [
    { when: (d) => d.isPremium, to: "premium-path" },
    { when: () => true, to: "standard-path" },
  ],
}

Resolver - Dynamic resolution with async logic:

next: {
  type: "resolver",
  resolve: async (data, ctx) => {
    const plan = await ctx.api.getPlan(data.userId);
    return plan.recommended;
  },
}

Type Safety

Your entire wizard is typed with your data:

type CheckoutData = {
  email: string;
  cardNumber: string;
};

const wizard: WizardDefinition<CheckoutData> = {
  // TypeScript ensures all steps match CheckoutData
  steps: {
    payment: {
      validate: (data) => {
        // data is CheckoutData
        data.cardNumber; // ✅ TypeScript knows this exists
        data.unknownField; // ❌ TypeScript error
      },
    },
  },
};

Building Wizards

With Builders (Recommended)

Use fluent API for readability:

import { createWizard } from "@gooonzick/wizard-core";

const wizard = createWizard<SignupData>("signup")
  .initialStep("personal")
  .step("personal", (s) =>
    s.title("Personal Info").required("name").next("contact"),
  )
  .step("contact", (s) =>
    s
      .title("Contact Info")
      .required("email")
      .previous("personal")
      .onSubmit(async (data) => {
        await api.submit(data);
      }),
  )
  .onComplete(async (data) => {
    console.log("Done!");
  })
  .build();

With Raw Definitions

For complex logic or server-side generation:

const wizard: WizardDefinition<SignupData> = {
  id: "signup",
  initialStepId: "personal",
  steps: {
    personal: {
      id: "personal",
      validate: (data) => ({
        valid: Boolean(data.name),
        errors: data.name ? undefined : { name: "Required" },
      }),
      next: { type: "static", to: "contact" },
    },
    contact: {
      id: "contact",
      validate: (data) => ({
        valid: data.email?.includes("@") ?? false,
        errors: data.email?.includes("@")
          ? undefined
          : { email: "Invalid email" },
      }),
      previous: { type: "static", to: "personal" },
    },
  },
};

For Simple Linear Flows

Use the linear helper:

const wizard = createLinearWizard<SignupData>({
  id: "signup",
  steps: [
    { id: "personal", title: "Name", validate: (d) => ({ valid: !!d.name }) },
    { id: "contact", title: "Email", validate: (d) => ({ valid: !!d.email }) },
    { id: "review", title: "Review" },
  ],
  onComplete: (data) => console.log("Done!", data),
});

Validation

Simple Validators

validate: (data) => ({
  valid: data.name?.length > 0,
  errors: data.name?.length > 0 ? undefined : { name: "Required" },
});

Using Utilities

import {
  combineValidators,
  requiredFields,
  createValidator,
} from "@gooonzick/wizard-core";

const emailValidator = createValidator(
  (data) => data.email?.includes("@"),
  "Invalid email",
  "email",
);

step.validate = combineValidators(
  requiredFields("name", "email"),
  emailValidator,
);

Schema Validation

import { createStandardSchemaValidator } from "@gooonzick/wizard-core";

step.validate = createStandardSchemaValidator(myValibotSchema);

Async Validation

validate: async (data, ctx) => {
  const isAvailable = await ctx.api.checkEmail(data.email);
  return {
    valid: isAvailable,
    errors: isAvailable ? undefined : { email: "Already taken" },
  };
};

Navigation

const machine = new WizardMachine(definition, context, initialData);

// Move forward
await machine.goNext();

// Move backward (uses history stack — always returns to actual previous step)
await machine.goPrevious();

// Jump to specific step (validates current step, checks target guards)
await machine.goTo("step-id");

// Jump without validation
await machine.goTo("step-id", { skipValidation: true });

// @deprecated — use goTo() instead
await machine.goToStep("step-id");

// Navigation history
console.log(machine.snapshot.canGoBack); // true if history has entries
console.log(machine.history); // full history stack
machine.clearHistory(); // reset history to current step only

// Query navigation
const canGo = await machine.canNavigateToStep("step-id");
const nextId = await machine.getNextStepId();
const available = await machine.getAvailableSteps();

Context and Dependencies

Pass utilities to validators and hooks via context:

interface MyContext extends WizardContext {
  api: ApiClient;
  logger: Logger;
}

const context: MyContext = {
  api: new ApiClient(),
  logger: console,
};

const machine = new WizardMachine(definition, context, initialData);

// Use in validators
const step = {
  validate: async (data, ctx) => {
    const myCtx = ctx as MyContext;
    const isValid = await myCtx.api.validate(data);
    return { valid: isValid };
  },
};

Events and Reactivity

Subscribe to state changes:

const machine = new WizardMachine(definition, context, initialData, {
  onStateChange: (state) => {
    // Update UI
  },
  onStepEnter: (stepId, data) => {
    // Load data for step
  },
  onValidation: (result) => {
    // Show validation errors
  },
  onComplete: (data) => {
    // Handle completion
  },
  onError: (error) => {
    // Handle errors
  },
});

TypeScript Support

Everything is generic over your data type:

// Define once
type SignupData = {
  name: string;
  email: string;
  plan: "basic" | "pro";
};

// Use everywhere
const wizard: WizardDefinition<SignupData> = { /* ... */ };
const machine: WizardMachine<SignupData> = new WizardMachine(wizard, ...);

// Types are inferred
machine.snapshot.data.name; // ✅ Works
machine.snapshot.data.unknown; // ❌ TypeScript error

Documentation

No Dependencies

The core library has zero dependencies. Optional schema validation requires Standard Schema implementations like:

  • Valibot
  • ArkType
  • Zod (with adapter)

License

MIT