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 HTMLNascondi 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 HTMLNascondi 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 HTMLNascondi 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 HTMLNascondi 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 HTMLNascondi 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 HTMLNascondi 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.
Mostra il codice HTMLNascondi 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 /> }]} />