Stepperize Documentation

repository·main·Indexed 23 days ago

https://github.com/damianricobelli/stepperize

A type-safe toolkit for building multi-step workflows, such as wizards, forms, and onboarding flows, for React and React Native. It includes @stepperize/react for state management and unstyled primitives, and @stepperize/core for framework-agnostic utilities, step mapping, and validation using the Standard Schema specification.

Tokens
55.4K
Snippets
161
Records
242
Agent score
79%

What's inside stepperize

  1. Introduction to Stepperize

    main

    Stepperize is a library for building typed multi-step flows in React. It allows you to define a sequence of named steps once, and then uses that single definition to drive hooks, providers, and UI primitives. This ensures that your step IDs, titles, and rendering logic remain synchronized and type-safe throughout your application.

    Common use cases include:

    • Checkout flows
    • Onboarding processes
    • Account setup
    • Surveys
    • Import wizards
    • Approval flows
    • Multi-step forms
  2. Stepperize Package Structure

    main

    The project is organized into several packages:

    • @stepperize/core: Framework-agnostic utilities and TypeScript types for step-based workflows.
    • @stepperize/react: React and React Native bindings, including defineStepper and unstyled primitives.
    • docs: The Stepperize documentation site, block gallery, changelog, and shadcn-compatible registry.
  3. Handle async navigation with `await`

    main

    Navigation methods like next, prev, goTo, and reset are asynchronous because they may trigger beforeStepChange guards. These methods return a Promise, which is always truthy. If you need to check if the navigation was actually accepted (e.g., a validation guard returned false), you must await the call.

    // ❌ `accepted` is a Promise, so this branch never runs
    const accepted = stepper.next();
    if (!accepted) return;
    
    // ✅ await to read the boolean result
    const accepted = await stepper.next();
    if (!accepted) return;
  4. Distinguish between step status and completion

    main

    Stepperize distinguishes between positional status and business-logic completion:

    • stepper.status(id): Returns the positional state based on the current index. Possible values are "active", "previous", or "upcoming".
    • stepper.isComplete(id): Represents explicit business state. A step is not considered complete just because the user passed it; you must explicitly set it using setComplete(id) or setComplete(id, false).
  5. How to style Stepperize primitives

    main
    Stepperize primitives are headless and ship with no styles. To style them, you must target the data-* attributes they expose. You can use Tailwind CSS variants, plain CSS attribute selectors, or any styling method that supports attribute targeting. The library manages the state via these attributes, while you maintain full control over the markup and appearance.
  6. Attach custom data to steps

    main

    Any properties you add to a step object (besides the required id) are available throughout the stepper instance. This is useful for rendering headers, icons, or handling per-step validation schemas.

    Commonly used custom fields:

    • title / description: For UI labels and headers.
    • schema: For per-step validation logic.
    • optional: For UI labels and completion rules.
    • icon: For stepper indicators.

    You can access these via the stepper.current object in your components.

    function Header() {
      const stepper = checkout.useStepper();
    
      return (
        <header>
          <h2>{stepper.current.title}</h2>
          <p>{stepper.current.description}</p>
        </header>
      );
    }
  7. Configure the linear navigation policy

    main

    The linear option (a boolean) controls trigger affordances like pointer and keyboard navigation. It does not restrict imperative navigation via stepper.goTo(id), which always bypasses the policy.

    • linear: false (default): canGoTo allows any known step; all triggers are enabled.
    • linear: true: canGoTo only allows previous steps, the current step, and the immediate next one. Primitive triggers and list keyboard navigation follow this constraint.

    Use stepper.canGoTo(id) to determine if a specific jump button or step trigger should be enabled in the UI.

    const checkout = defineStepper(steps, {
      linear: true,
    });
    
    // In your UI:
    <button
      type="button"
      disabled={!stepper.canGoTo("review")}
      onClick={() => stepper.goTo("review")}
    >
      Review
    </button>
  8. How Stepperize scales with your requirements

    main

    Stepperize is designed to be additive. You can start with simple local state and move to more complex architectures without rewriting your core logic:

    1. Local: Use useStepper() within a single component with your own custom markup. This is the starting point for most flows.
    2. Guarded: Implement beforeStepChange to handle validation or data saving before a user moves to the next step.
    3. Drafts: Use the data property when steps require per-step data for persistence or review.
    4. Shared: Use a Provider when your stepper UI is split across separate components (e.g., a header, a main panel, and a footer).
    5. Primitives: Use the built-in Stepper.* components for a fully accessible and styled stepper UI.
  9. Configure TypeScript for type-safe step IDs

    main

    To ensure maximum type safety, define your stepper using an inline array within defineStepper. This allows TypeScript to treat the id values as literal types (e.g., "shipping" | "payment" | "review"). This prevents runtime errors by causing TypeScript to throw an error if you attempt to navigate to an invalid ID, such as stepper.goTo("paymant").

    const checkout = defineStepper([
      { id: "shipping", title: "Shipping" },
      { id: "payment", title: "Payment" },
      { id: "review", title: "Review" },
    ]);
  10. Understand `linear` navigation constraints

    main
    When linear is enabled (the default), Stepper.Trigger components are disabled if the target step is more than one step ahead of the current step. This is a navigation policy, not a bug. If you need to allow jumping, set linear={false} in your configuration or use the imperative stepper.goTo(id) method, which always bypasses this policy.
  11. Mapping React Hook Form to Stepperize patterns

    main

    When integrating React Hook Form with Stepperize, use the following mapping for core patterns:

    PatternReact Hook Form Implementation
    Seed from draftuseForm({ defaultValues: stepper.data.get(id) })
    Field validationresolver: zodResolver(schema)
    Save + advancehandleSubmit(async (data) => stepper.next({ data: data }))
    Navigation gate (optional)beforeStepChange on the Provider/instance
    Reviewstepper.data.all() (no RHF required)
    Edit previousstepper.goTo(id) (triggers RHF re-seeding)
  12. Manage stepper state with useStepper(), Provider, and Controlled options

    main

    Stepperize provides three primary ways to manage state depending on your UI architecture:

    1. Local State (useStepper()): Best for compact wizards where the content, actions, and progress live within a single component. If called outside a Provider or Stepper.Root, it creates a new local instance.
    2. Shared State (Provider): Best when the UI is split across multiple components. By wrapping descendants in a generated checkout.Provider, any descendant calling checkout.useStepper() will read the shared instance from context instead of creating a new one.
    3. Controlled State: Best when an external source (Router, URL, Store, or Server) owns the source of truth. You can pass options like step, onStepChange, data, onDataChange, completed, and onCompletedChange to useStepper() to sync the stepper with external state.
    import { defineStepper } from "@stepperize/react";
    
    const checkout = defineStepper([
      { id: "shipping", title: "Shipping" },
      { id: "payment", title: "Payment" },
      { id: "review", title: "Review" },
    ]);
    
    // 1. Local
    function Checkout() {
      const stepper = checkout.useStepper();
      return <Panel stepper={stepper} />;
    }
    
    // 2. Provider
    function CheckoutShell() {
      return (
        <checkout.Provider defaultStep="shipping">
          <Sidebar />
          <Panel />
        </checkout.Provider>
      );
    }
    
    // 3. Controlled
    const stepper = checkout.useStepper({
      step,
      onStepChange: setStep,
      data: values,
      onDataChange: setValues,
      completed,
      onCompletedChange: setCompleted,
    });