Vai al contenuto

Componente

Stepper

L'intestazione dei moduli a più passaggi, con passi completati, passo corrente e navigazione Indietro/Avanti con punti o barra di avanzamento.

Riferimento
<it-steppers>
Classi
ita-stepperita-stepper-headerita-stepper-doneita-stepper-markita-stepper-iconita-stepper-checkita-stepper-countita-stepper-navita-stepper-progressita-stepper-dotsita-stepper-titleita-steps
Basato su (daisyUI)
stepsstepstep-primarysteps-horizontalsteps-verticalprogressbtn
Solo CSS
Lo stato arriva dal server: current indica il passo attivo, i precedenti sono completati. Indietro e Avanti sono pulsanti di invio (name="passo"): ogni passo è un giro dal server, e Indietro usa formnovalidate. Con panels il componente include i pannelli dei passi in un <form data-stepper>. Per cambiare passo nella pagina senza ricaricarla serve JavaScript: vedi sotto. Il passo corrente ha aria-current="step". Sotto lg l'intestazione mostra solo il passo corrente e l'indice «2/5».
Storybook
Apri le storie di Stepper

Esempio base

Passo 2 di 5

Mostra il codice HTML
<div class="ita-stepper">
  <nav aria-label="Passaggi della domanda">
  <ol class="ita-stepper-header">
    <li class="ita-stepper-done"><span>Informativa</span><span class="sr-only"> (completato)</span><svg class="ita-stepper-check shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9.6,16.9L4,11.4l0.8-0.7l4.8,4.8l8.5-8.4l0.7,0.7L9.6,16.9z" /></svg></li>
    <li aria-current="step"><span>Dati personali</span><span class="ita-stepper-count" aria-hidden="true">2/5</span></li>
    <li><span>Documenti</span></li>
    <li><span>Pagamento</span></li>
    <li><span>Riepilogo</span></li>
  </ol>
  </nav>
  <div class="ita-stepper-nav">
    <button type="submit" name="passo" value="0" formnovalidate class="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs" data-stepper-back><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="14.6,17.2 9,11.6 14.6,6 15.4,6.7 10.5,11.6 15.4,16.5" /></svg><span>Indietro</span></button>
    <div class="ita-stepper-dots"><p class="sr-only" aria-live="polite" data-stepper-status>Passo 2 di 5</p><ul aria-hidden="true" data-stepper-dots><li></li><li data-current></li><li></li><li></li><li></li></ul></div>
    <button type="submit" name="passo" value="2" class="ita-btn ita-btn-primary ita-btn-xs" data-stepper-next data-confirm="Conferma"><span>Avanti</span><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="9.8,17.2 9,16.5 13.9,11.6 9,6.7 9.8,6 15.4,11.6" /></svg></button>
  </div>
</div>

Con numeri

Passo 3 di 5

Mostra il codice HTML
<div class="ita-stepper">
  <nav aria-label="Passaggi della domanda">
  <ol class="ita-stepper-header">
    <li class="ita-stepper-done"><span class="ita-stepper-mark" aria-hidden="true"><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9.6,16.9L4,11.4l0.8-0.7l4.8,4.8l8.5-8.4l0.7,0.7L9.6,16.9z" /></svg></span><span>Informativa</span><span class="sr-only"> (completato)</span></li>
    <li class="ita-stepper-done"><span class="ita-stepper-mark" aria-hidden="true"><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9.6,16.9L4,11.4l0.8-0.7l4.8,4.8l8.5-8.4l0.7,0.7L9.6,16.9z" /></svg></span><span>Dati personali</span><span class="sr-only"> (completato)</span></li>
    <li aria-current="step"><span class="ita-stepper-mark" aria-hidden="true">3</span><span>Documenti</span><span class="ita-stepper-count" aria-hidden="true">3/5</span></li>
    <li><span class="ita-stepper-mark" aria-hidden="true">4</span><span>Pagamento</span></li>
    <li><span class="ita-stepper-mark" aria-hidden="true">5</span><span>Riepilogo</span></li>
  </ol>
  </nav>
  <div class="ita-stepper-nav">
    <button type="submit" name="passo" value="1" formnovalidate class="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs" data-stepper-back><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="14.6,17.2 9,11.6 14.6,6 15.4,6.7 10.5,11.6 15.4,16.5" /></svg><span>Indietro</span></button>
    <div class="ita-stepper-dots"><p class="sr-only" aria-live="polite" data-stepper-status>Passo 3 di 5</p><ul aria-hidden="true" data-stepper-dots><li></li><li></li><li data-current></li><li></li><li></li></ul></div>
    <button type="submit" name="passo" value="3" class="ita-btn ita-btn-primary ita-btn-xs" data-stepper-next data-confirm="Conferma"><span>Avanti</span><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="9.8,17.2 9,16.5 13.9,11.6 9,6.7 9.8,6 15.4,11.6" /></svg></button>
  </div>
</div>

Con icone

Passo 4 di 5

Mostra il codice HTML
<div class="ita-stepper">
  <nav aria-label="Passaggi della domanda">
  <ol class="ita-stepper-header">
    <li class="ita-stepper-done"><svg class="ita-stepper-icon shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z M12,21c-5,0-9-4-9-9s4-9,9-9s9,4,9,9S17,21,12,21L12,21z M11.3,6h1.5v2h-1.5V6z M11.3,9h1.5v9h-1.5V9z" /></svg><span>Informativa</span><span class="sr-only"> (completato)</span><svg class="ita-stepper-check shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9.6,16.9L4,11.4l0.8-0.7l4.8,4.8l8.5-8.4l0.7,0.7L9.6,16.9z" /></svg></li>
    <li class="ita-stepper-done"><svg class="ita-stepper-icon shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12,13c2.2,0,4-1.8,4-4V6c0-2.2-1.8-4-4-4S8,3.8,8,6v3C8,11.2,9.8,13,12,13z M9,6c0-1.7,1.3-3,3-3s3,1.3,3,3v3 c0,1.7-1.3,3-3,3s-3-1.3-3-3V6z M20,22h-1c0-3.9-3.1-7-7-7s-7,3.1-7,7H4c0-4.4,3.6-8,8-8S20,17.6,20,22z" /></svg><span>Dati personali</span><span class="sr-only"> (completato)</span><svg class="ita-stepper-check shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9.6,16.9L4,11.4l0.8-0.7l4.8,4.8l8.5-8.4l0.7,0.7L9.6,16.9z" /></svg></li>
    <li class="ita-stepper-done"><svg class="ita-stepper-icon shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.7,2H7.5C6.7,2,6,2.7,6,3.5V4H5.5C4.7,4,4,4.7,4,5.5v15C4,21.3,4.7,22,5.5,22h11c0.8,0,1.5-0.7,1.5-1.5V20h0.5 c0.8,0,1.5-0.7,1.5-1.5V6.3L15.7,2z M16,3.7L18.3,6h-1.8C16.2,6,16,5.8,16,5.5V3.7z M17,20.5c0,0.3-0.2,0.5-0.5,0.5h-11 C5.2,21,5,20.8,5,20.5v-15C5,5.2,5.2,5,5.5,5H6v13.5C6,19.3,6.7,20,7.5,20H17V20.5z M18.5,19h-11C7.2,19,7,18.8,7,18.5v-15 C7,3.2,7.2,3,7.5,3H15v2.5C15,6.3,15.7,7,16.5,7H19v11.5C19,18.8,18.8,19,18.5,19z M9,9h8v1H9V9z M9,11h8v1H9V11z M9,13h4v1H9V13z" /></svg><span>Documenti</span><span class="sr-only"> (completato)</span><svg class="ita-stepper-check shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9.6,16.9L4,11.4l0.8-0.7l4.8,4.8l8.5-8.4l0.7,0.7L9.6,16.9z" /></svg></li>
    <li aria-current="step"><svg class="ita-stepper-icon shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20.5,5h-17C2.7,5,2,5.7,2,6.5v11C2,18.3,2.7,19,3.5,19h17c0.8,0,1.5-0.7,1.5-1.5v-11C22,5.7,21.3,5,20.5,5z M3,9h18v3H3V9 z M21,17.5c0,0.3-0.2,0.5-0.5,0.5h-17C3.2,18,3,17.8,3,17.5V13h18V17.5z M3,8V6.5C3,6.2,3.2,6,3.5,6h17C20.8,6,21,6.2,21,6.5V8H3z M8,15H4v-1h4V15z" /></svg><span>Pagamento</span><span class="ita-stepper-count" aria-hidden="true">4/5</span></li>
    <li><svg class="ita-stepper-icon shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M17.1,7.9l0.7,0.7L10,16.2l-4.7-4.6L6,10.9l4,3.9L17.1,7.9z M22,12c0,5.5-4.5,10-10,10S2,17.5,2,12S6.5,2,12,2S22,6.5,22,12z M21,12c0-5-4-9-9-9s-9,4-9,9s4,9,9,9S21,17,21,12z" /></svg><span>Riepilogo</span></li>
  </ol>
  </nav>
  <div class="ita-stepper-nav">
    <button type="submit" name="passo" value="2" formnovalidate class="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs" data-stepper-back><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="14.6,17.2 9,11.6 14.6,6 15.4,6.7 10.5,11.6 15.4,16.5" /></svg><span>Indietro</span></button>
    <div class="ita-stepper-progress"><p class="sr-only" aria-live="polite" data-stepper-status>Passo 4 di 5</p><progress value="4" max="5" aria-hidden="true" data-stepper-progress></progress></div>
    <button type="submit" name="passo" value="4" class="ita-btn ita-btn-primary ita-btn-xs" data-stepper-next data-confirm="Conferma"><span>Avanti</span><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="9.8,17.2 9,16.5 13.9,11.6 9,6.7 9.8,6 15.4,11.6" /></svg></button>
  </div>
</div>

Cerchi uniti (daisyUI steps)

L'alternativa daisyUI: su mobile i passi diventano verticali.

Passo 3 di 5

Mostra il codice HTML
<div class="ita-stepper">
  <nav aria-label="Passaggi della domanda">
  <ol class="steps steps-vertical sm:steps-horizontal ita-steps">
    <li class="step step-primary" data-content="✓">Informativa<span class="sr-only"> (completato)</span></li>
    <li class="step step-primary" data-content="✓">Dati personali<span class="sr-only"> (completato)</span></li>
    <li class="step step-primary" aria-current="step">Documenti</li>
    <li class="step">Pagamento</li>
    <li class="step">Riepilogo</li>
  </ol>
  </nav>
  <div class="ita-stepper-nav">
    <button type="submit" name="passo" value="1" formnovalidate class="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs" data-stepper-back><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="14.6,17.2 9,11.6 14.6,6 15.4,6.7 10.5,11.6 15.4,16.5" /></svg><span>Indietro</span></button>
    <div class="ita-stepper-progress"><p class="sr-only" aria-live="polite" data-stepper-status>Passo 3 di 5</p><progress value="3" max="5" aria-hidden="true" data-stepper-progress></progress></div>
    <button type="submit" name="passo" value="3" class="ita-btn ita-btn-primary ita-btn-xs" data-stepper-next data-confirm="Conferma"><span>Avanti</span><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="9.8,17.2 9,16.5 13.9,11.6 9,6.7 9.8,6 15.4,11.6" /></svg></button>
  </div>
</div>

Ultimo passo

Passo 3 di 3

Mostra il codice HTML
<div class="ita-stepper">
  <nav aria-label="Passaggi della domanda">
  <ol class="ita-stepper-header">
    <li class="ita-stepper-done"><span class="ita-stepper-mark" aria-hidden="true"><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9.6,16.9L4,11.4l0.8-0.7l4.8,4.8l8.5-8.4l0.7,0.7L9.6,16.9z" /></svg></span><span>Dati</span><span class="sr-only"> (completato)</span></li>
    <li class="ita-stepper-done"><span class="ita-stepper-mark" aria-hidden="true"><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9.6,16.9L4,11.4l0.8-0.7l4.8,4.8l8.5-8.4l0.7,0.7L9.6,16.9z" /></svg></span><span>Allegati</span><span class="sr-only"> (completato)</span></li>
    <li aria-current="step"><span class="ita-stepper-mark" aria-hidden="true">3</span><span>Riepilogo</span><span class="ita-stepper-count" aria-hidden="true">3/3</span></li>
  </ol>
  </nav>
  <div class="ita-stepper-nav">
    <button type="submit" name="passo" value="1" formnovalidate class="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs" data-stepper-back><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="14.6,17.2 9,11.6 14.6,6 15.4,6.7 10.5,11.6 15.4,16.5" /></svg><span>Indietro</span></button>
    <div class="ita-stepper-dots"><p class="sr-only" aria-live="polite" data-stepper-status>Passo 3 di 3</p><ul aria-hidden="true" data-stepper-dots><li></li><li></li><li data-current></li></ul></div>
    <button type="submit" name="passo" value="invia" class="ita-btn ita-btn-success ita-btn-xs" data-stepper-next data-confirm="Invia la domanda"><span>Invia la domanda</span><svg class="hidden shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="9.8,17.2 9,16.5 13.9,11.6 9,6.7 9.8,6 15.4,11.6" /></svg></button>
  </div>
</div>

Su sfondo primario

Il contenitore ha la classe it-surface-primary: i token si scambiano (base diventa primary, primary diventa primary-content) e il componente si adatta senza opzioni.

Passo 2 di 5

Mostra il codice HTML
<div class="it-surface-primary rounded-box p-6"><div class="ita-stepper">
  <nav aria-label="Passaggi della domanda">
  <ol class="ita-stepper-header">
    <li class="ita-stepper-done"><span class="ita-stepper-mark" aria-hidden="true"><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9.6,16.9L4,11.4l0.8-0.7l4.8,4.8l8.5-8.4l0.7,0.7L9.6,16.9z" /></svg></span><span>Informativa</span><span class="sr-only"> (completato)</span></li>
    <li aria-current="step"><span class="ita-stepper-mark" aria-hidden="true">2</span><span>Dati personali</span><span class="ita-stepper-count" aria-hidden="true">2/5</span></li>
    <li><span class="ita-stepper-mark" aria-hidden="true">3</span><span>Documenti</span></li>
    <li><span class="ita-stepper-mark" aria-hidden="true">4</span><span>Pagamento</span></li>
    <li><span class="ita-stepper-mark" aria-hidden="true">5</span><span>Riepilogo</span></li>
  </ol>
  </nav>
  <div class="ita-stepper-nav">
    <button type="submit" name="passo" value="0" formnovalidate class="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs" data-stepper-back><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="14.6,17.2 9,11.6 14.6,6 15.4,6.7 10.5,11.6 15.4,16.5" /></svg><span>Indietro</span></button>
    <div class="ita-stepper-dots"><p class="sr-only" aria-live="polite" data-stepper-status>Passo 2 di 5</p><ul aria-hidden="true" data-stepper-dots><li></li><li data-current></li><li></li><li></li><li></li></ul></div>
    <button type="submit" name="passo" value="2" class="ita-btn ita-btn-primary ita-btn-xs" data-stepper-next data-confirm="Conferma"><span>Avanti</span><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="9.8,17.2 9,16.5 13.9,11.6 9,6.7 9.8,6 15.4,11.6" /></svg></button>
  </div>
</div></div>

Modulo a passi

Con panels il componente diventa un <form>: senza JavaScript Avanti invia il passo al server. Con il frammento JavaScript qui sotto i passi cambiano nella pagina, e prima di andare avanti controlla i campi del passo corrente.

Informativa

Prima di iniziare leggi l'informativa sul trattamento dei dati personali.

Passo 1 di 5

Mostra il codice HTML
<form action="#" method="post" class="ita-stepper" data-stepper>
  <nav aria-label="Passaggi della domanda">
  <ol class="steps steps-vertical sm:steps-horizontal ita-steps">
    <li class="step step-primary" aria-current="step">Informativa</li>
    <li class="step">Dati personali</li>
    <li class="step">Documenti</li>
    <li class="step">Pagamento</li>
    <li class="step">Riepilogo</li>
  </ol>
  </nav>
  <section data-step aria-labelledby="stepper-7-1-title">
    <h3 id="stepper-7-1-title" tabindex="-1" class="ita-stepper-title">Informativa</h3>
    <p class="mb-4">Prima di iniziare leggi l'informativa sul trattamento dei dati personali.</p>
    <label class="flex cursor-pointer items-center gap-3"><input type="checkbox" name="informativa" required class="ita-checkbox"><span>Ho letto l'informativa</span></label>
  </section>
  <section data-step aria-labelledby="stepper-7-2-title" hidden>
    <h3 id="stepper-7-2-title" tabindex="-1" class="ita-stepper-title">Dati personali</h3>
    <div class="grid max-w-2xl gap-4 md:grid-cols-2">
      <div><label for="st-nome" class="mb-1 block font-semibold">Nome</label><input id="st-nome" name="nome" required autocomplete="given-name" class="ita-input"></div>
      <div><label for="st-cognome" class="mb-1 block font-semibold">Cognome</label><input id="st-cognome" name="cognome" required autocomplete="family-name" class="ita-input"></div>
    </div>
  </section>
  <section data-step aria-labelledby="stepper-7-3-title" hidden>
    <h3 id="stepper-7-3-title" tabindex="-1" class="ita-stepper-title">Documenti</h3>
    <label for="st-doc" class="mb-1 block font-semibold">Documento d'identità</label>
    <input id="st-doc" type="file" name="documento" class="ita-file-input max-w-md">
  </section>
  <section data-step aria-labelledby="stepper-7-4-title" hidden>
    <h3 id="stepper-7-4-title" tabindex="-1" class="ita-stepper-title">Pagamento</h3>
    <p>Il costo del servizio è di 16,00 €, pagabile con pagoPA al termine della domanda.</p>
  </section>
  <section data-step aria-labelledby="stepper-7-5-title" hidden>
    <h3 id="stepper-7-5-title" tabindex="-1" class="ita-stepper-title">Riepilogo</h3>
    <p>Controlla i dati inseriti, poi premi «Invia la domanda».</p>
  </section>
  <div class="ita-stepper-nav">
    <button type="submit" name="passo" value="0" formnovalidate class="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs" disabled data-stepper-back><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="14.6,17.2 9,11.6 14.6,6 15.4,6.7 10.5,11.6 15.4,16.5" /></svg><span>Indietro</span></button>
    <div class="ita-stepper-progress"><p class="sr-only" aria-live="polite" data-stepper-status>Passo 1 di 5</p><progress value="1" max="5" aria-hidden="true" data-stepper-progress></progress></div>
    <button type="submit" name="passo" value="1" class="ita-btn ita-btn-primary ita-btn-xs" data-stepper-next data-confirm="Invia la domanda"><span>Avanti</span><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><polygon points="9.8,17.2 9,16.5 13.9,11.6 9,6.7 9.8,6 15.4,11.6" /></svg></button>
  </div>
</form>

Con JavaScript (opzionale)

La ricetta è solo CSS e funziona così com'è. Questi frammenti non sono inclusi nel pacchetto: mostrano come aggiungere il comportamento che richiede uno script, in JavaScript semplice o in React.

Passi nella stessa paginajs

Per il markup di stepper({ panels, header: "steps" }). Intercetta Indietro e Avanti, controlla solo i campi del passo visibile, aggiorna intestazione, barra o punti, e sposta il focus sul titolo del nuovo passo. All'ultimo passo il pulsante invia davvero il form.

document.querySelectorAll("form[data-stepper]").forEach((form) => {
  const panels = [...form.querySelectorAll("[data-step]")];
  const steps = [...form.querySelectorAll("nav ol > li")];
  const back = form.querySelector("[data-stepper-back]");
  const next = form.querySelector("[data-stepper-next]");
  const bar = form.querySelector("[data-stepper-progress]");
  const dots = [...form.querySelectorAll("[data-stepper-dots] li")];
  const status = form.querySelector("[data-stepper-status]");
  const last = panels.length - 1;
  let current = Math.max(0, panels.findIndex((p) => !p.hidden));

  function show(i, focus = true) {
    current = i;
    panels.forEach((p, k) => (p.hidden = k !== i));
    // daisyUI steps header: step-primary up to the current step, a check on the done ones
    steps.forEach((li, k) => {
      li.classList.toggle("step-primary", k <= i);
      if (k < i) li.dataset.content = "✓";
      else delete li.dataset.content;
      li.querySelector(".sr-only")?.remove();
      if (k < i) li.insertAdjacentHTML("beforeend", '<span class="sr-only"> (completato)</span>');
      if (k === i) li.setAttribute("aria-current", "step");
      else li.removeAttribute("aria-current");
    });
    dots.forEach((d, k) => d.toggleAttribute("data-current", k === i));
    if (bar) bar.value = i + 1;
    if (status) status.textContent = "Passo " + (i + 1) + " di " + panels.length;
    back.disabled = i === 0;
    const isLast = i === last;
    next.querySelector("span").textContent = isLast ? next.dataset.confirm : "Avanti";
    next.querySelector("svg")?.classList.toggle("hidden", isLast);
    next.classList.toggle("ita-btn-success", isLast);
    next.classList.toggle("ita-btn-primary", !isLast);
    if (focus) panels[i].querySelector("[tabindex='-1']")?.focus();
  }

  back.addEventListener("click", (ev) => {
    ev.preventDefault();
    show(current - 1);
  });
  next.addEventListener("click", (ev) => {
    if (current === last) return; // last step: a real submit, validated by the browser
    ev.preventDefault();
    const invalid = [...panels[current].querySelectorAll("input, select, textarea")].find((el) => !el.checkValidity());
    if (invalid) return invalid.reportValidity();
    show(current + 1);
  });
  show(current, false);
});

Componente Reacttsx

React tiene l'indice del passo; ogni passo è un fieldset e viene validato prima di andare avanti.

import { useRef, useState, type FormEvent, type ReactNode } from "react";

type Step = { label: string; content: ReactNode };

export function StepperForm({ steps, onSubmit, confirmLabel = "Conferma" }: {
  steps: Step[];
  onSubmit: (data: FormData) => void;
  confirmLabel?: string;
}) {
  const [current, setCurrent] = useState(0);
  const panels = useRef<(HTMLFieldSetElement | null)[]>([]);
  const titles = useRef<(HTMLHeadingElement | null)[]>([]);
  const last = steps.length - 1;

  const go = (i: number) => {
    setCurrent(i);
    requestAnimationFrame(() => titles.current[i]?.focus());
  };
  const next = () => {
    const invalid = [...(panels.current[current]?.elements ?? [])]
      .find((el) => "checkValidity" in el && !(el as HTMLInputElement).checkValidity()) as HTMLInputElement | undefined;
    if (invalid) return invalid.reportValidity();
    go(current + 1);
  };
  const submit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (current < last) return next(); // Enter in a field moves forward
    onSubmit(new FormData(e.currentTarget));
  };

  return (
    <form onSubmit={submit} noValidate={current < last} className="ita-stepper">
      <nav aria-label="Passaggi della domanda">
        <ol className="steps steps-vertical sm:steps-horizontal ita-steps">
          {steps.map((s, i) => (
            <li key={s.label} aria-current={i === current ? "step" : undefined}
                data-content={i < current ? "✓" : undefined}
                className={"step" + (i <= current ? " step-primary" : "")}>
              {s.label}{i < current && <span className="sr-only"> (completato)</span>}
            </li>
          ))}
        </ol>
      </nav>

      {steps.map((s, i) => (
        // Hidden steps stay mounted so their values are still posted.
        <fieldset key={s.label} ref={(el) => { panels.current[i] = el; }} hidden={i !== current}>
          <h3 ref={(el) => { titles.current[i] = el; }} tabIndex={-1} className="ita-stepper-title">{s.label}</h3>
          {s.content}
        </fieldset>
      ))}

      <div className="ita-stepper-nav">
        <button type="button" className="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs" disabled={current === 0} onClick={() => go(current - 1)}>
          ‹ Indietro
        </button>
        <p className="sr-only" aria-live="polite">Passo {current + 1} di {steps.length}</p>
        <div className="ita-stepper-progress"><progress value={current + 1} max={steps.length} aria-hidden /></div>
        {current < last ? (
          <button type="button" className="ita-btn ita-btn-primary ita-btn-xs" onClick={next}>Avanti ›</button>
        ) : (
          <button type="submit" className="ita-btn ita-btn-success ita-btn-xs">{confirmLabel}</button>
        )}
      </div>
    </form>
  );
}

// <StepperForm confirmLabel="Invia la domanda" onSubmit={(data) => fetch("/api/domanda", { method: "POST", body: data })}
//   steps={[{ label: "Dati personali", content: <DatiPersonali /> }, { label: "Documenti", content: <Documenti /> }]} />