Skip to content

FormWizard

FormWizard and WizardStep build a server-driven multi-step form flow. They are designed for HTMX partial replacement: your server decides the current step, then returns the wizard again.

Use Stepper when you only need progress display. Use FormWizard when the user is actively moving through form panels.

Import

from faststrap import FormWizard, WizardStep, Input

Basic Usage

FormWizard(
    WizardStep(
        "Account",
        Input("email", input_type="email", label="Email"),
    ),
    WizardStep(
        "Profile",
        Input("company", label="Company"),
    ),
    current_step=0,
    endpoint="/setup",
    hx_target="#setup-wizard",
    id="setup-wizard",
)

Complete HTMX Flow

from fasthtml.common import fast_app
from faststrap import FormWizard, WizardStep, Input

app, rt = fast_app()


def setup_wizard(step: int = 0):
    return FormWizard(
        WizardStep("Account", Input("email", input_type="email", label="Email")),
        WizardStep("Profile", Input("company", label="Company")),
        WizardStep("Finish", Input("plan", label="Plan")),
        current_step=step,
        endpoint="/setup",
        hx_target="#setup-wizard",
        id="setup-wizard",
    )


@rt("/")
def home():
    return setup_wizard()


@rt("/setup", methods=["POST"])
async def setup(request):
    form = await request.form()
    step = int(form.get("step", 0))
    return setup_wizard(step)

The wizard posts a step value when the user clicks Back, Next, or Finish. Your route can validate the current step before returning the next one.

Parameters

Param Type Description
*steps Any WizardStep panels.
current_step int Zero-based active step index.
endpoint str | None Form/HTMX endpoint.
method get | post Submission method.
step_name str Submitted field name for the next step index.
next_label str Next button label.
previous_label str Previous button label.
finish_label str Final step button label.
show_stepper bool Show a progress stepper above the current panel.
hx_target str | None HTMX target for partial replacement.

faststrap.components.forms.form_wizard.FormWizard(*steps, current_step=0, endpoint=None, method='post', step_name='step', next_label='Next', previous_label='Back', finish_label='Finish', show_stepper=True, hx_target=None, hx_swap='outerHTML', controls_cls=None, **kwargs)

Render a server-driven multi-step form shell.

Source code in src/faststrap/components/forms/form_wizard.py
@register(category="forms")
@beta
def FormWizard(
    *steps: Any,
    current_step: int = 0,
    endpoint: str | None = None,
    method: WizardMethod = "post",
    step_name: str = "step",
    next_label: str = "Next",
    previous_label: str = "Back",
    finish_label: str = "Finish",
    show_stepper: bool = True,
    hx_target: str | None = None,
    hx_swap: str = "outerHTML",
    controls_cls: str | None = None,
    **kwargs: Any,
) -> Div:
    """Render a server-driven multi-step form shell."""
    if method not in {"get", "post"}:
        msg = f"method must be 'get' or 'post', got {method!r}"
        raise ValueError(msg)
    if not steps:
        msg = "FormWizard requires at least one WizardStep or content panel."
        raise ValueError(msg)

    safe_step = max(0, min(current_step, len(steps) - 1))
    user_cls = kwargs.pop("cls", "")
    attrs: dict[str, Any] = {
        "cls": merge_classes("faststrap-form-wizard", user_cls),
        "data_fs_form_wizard": "true",
        "data_current_step": str(safe_step),
    }
    attrs.update(convert_attrs(kwargs))

    stepper_items = []
    for index, step in enumerate(steps):
        title = getattr(step, "attrs", {}).get("data-title", f"Step {index + 1}")
        description = getattr(step, "attrs", {}).get("data-description")
        icon = getattr(step, "attrs", {}).get("data-icon")
        if index < safe_step:
            status: StepStatus = "complete"
        elif index == safe_step:
            status = "current"
        else:
            status = "pending"
        stepper_items.append(
            StepperStep(
                title,
                description=description,
                status=status,
                step=index + 1,
                icon=icon,
            )
        )

    form_attrs: dict[str, Any] = {
        "method": method,
        "cls": "faststrap-form-wizard-form",
    }
    if endpoint:
        form_attrs["action"] = endpoint
        form_attrs[f"hx_{method}"] = endpoint
        if hx_target:
            form_attrs["hx_target"] = hx_target
        form_attrs["hx_swap"] = hx_swap

    previous_step = max(0, safe_step - 1)
    next_step = min(len(steps) - 1, safe_step + 1)
    controls = []
    if safe_step > 0:
        controls.append(
            Button(
                previous_label,
                type="submit",
                name=step_name,
                value=str(previous_step),
                variant="secondary",
                outline=True,
            )
        )
    controls.append(
        Button(
            finish_label if safe_step == len(steps) - 1 else next_label,
            type="submit",
            name=step_name,
            value=str(next_step),
            variant="primary",
        )
    )

    parts: list[Any] = []
    if show_stepper:
        parts.append(Stepper(*stepper_items, cls="mb-4"))
    parts.append(
        FTForm(
            steps[safe_step],
            Div(
                *controls,
                cls=merge_classes("d-flex justify-content-between gap-2 mt-4", controls_cls),
            ),
            **convert_attrs(form_attrs),
        )
    )

    return Div(*parts, **attrs)

faststrap.components.forms.form_wizard.WizardStep(title, *content, description=None, icon=None, **kwargs)

Render one FormWizard step panel.

Source code in src/faststrap/components/forms/form_wizard.py
@register(category="forms")
@beta
def WizardStep(
    title: str,
    *content: Any,
    description: str | None = None,
    icon: str | None = None,
    **kwargs: Any,
) -> Div:
    """Render one FormWizard step panel."""
    user_cls = kwargs.pop("cls", "")
    attrs: dict[str, Any] = {
        "cls": merge_classes("faststrap-wizard-step", user_cls),
        "data_fs_wizard_step": "true",
        "data_title": title,
    }
    if description:
        attrs["data_description"] = description
    if icon:
        attrs["data_icon"] = icon
    attrs.update(convert_attrs(kwargs))
    return Div(*content, **attrs)