material-stepper

Multi-step forms / wizard — M2 anatomy with M3 tokens (M3 has no stepper spec). Client-side with constraint-validation gating, or a server-driven indicator for django-formtools WizardView.

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"

Everything filled in — submit posts the whole form (request.POST sees all steps' fields).

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>