client-side wizard — one form, all steps in the DOM (values survive
Back), data-stepper-next gates on the active step's
constraint validation (native bubble on the first invalid field, the
step turns red until it passes); the last step's button is an ordinary
type="submit"
events: —
orientation="vertical" — content folds out under each
header, the connector runs through collapsed steps
Settings summary…
Ad creative…
server-driven indicator (django-formtools) — steps carry no content;
the template marks active/completed/error,
a click on a visited step emits cancelable materialStepClick
→ post wizard_goto_step
wizard_goto_step: —
linear="false" — any header is clickable, steps complete
in any order
Basics form…
Attachments form…
Permissions form…
RTL — circles, connectors and the vertical content border mirror with the reading direction
Sample usage (django-formtools WizardView)
<material-stepper id="wizard-steps">
{% for step in wizard.steps.all %}
<material-step label="{{ step|capfirst }}" value="{{ step }}"
{% if step == wizard.steps.current %}active{% endif %}
{% if forloop.counter0 < wizard.steps.step0 %}completed{% endif %}>
</material-step>
{% endfor %}
</material-stepper>
<form method="post">{% csrf_token %}
{{ wizard.management_form }}
{{ wizard.form }}
{% if wizard.steps.prev %}
<button name="wizard_goto_step" value="{{ wizard.steps.prev }}">Back</button>
{% endif %}
<button type="submit">{% if wizard.steps.next %}Continue{% else %}Finish{% endif %}</button>
</form>
<script>
// header click on a visited step → jump the wizard server-side
document.getElementById('wizard-steps').addEventListener('materialStepClick', (e) => {
e.preventDefault();
const input = Object.assign(document.createElement('input'),
{ type: 'hidden', name: 'wizard_goto_step', value: e.detail.value });
const form = document.querySelector('form');
form.appendChild(input);
form.submit();
});
</script>