ITS Web Design e Strategie Digitali
ITS Academy I-CREA @ CFP Bauer

Guida interattiva ai

Form HTML

Costruire ponti interattivi tra utente e server

Onnipresenti e indispensabili

I moduli (form) HTML sono il meccanismo principale per raccogliere input dagli utenti. Sono ovunque nell'esperienza web quotidiana: login, registrazione, ricerca, e-commerce, sondaggi, commenti.

Trasformano la navigazione da passiva (leggere contenuti) ad attiva (interagire, inviare informazioni).

L'elemento cardine per la creazione di un modulo è il tag <form>. Questo elemento agisce da contenitore per tutti i controlli — campi di testo, pulsanti, checkbox, ecc. — che costituiscono un modulo specifico.

Cosa imparerete:

  • Come creare la struttura di un form e inviare dati a un server
  • Tutti i tipi di controlli: testo, selezione, checkbox, date, file...
  • Come raggruppare e validare i dati
  • I principi di accessibilità nei form

L'Anatomia di un Form

Un form HTML è composto da diversi "pezzi" che lavorano insieme. Ecco una mappa mentale per orientarvi:

<form>

<form> ... </form>

Il contenitore: dove e come inviare i dati

<input> text

Campo testo a riga singola

<input> checkbox / radio

Scelte multiple o singole

<input> date / color / range

Controlli specializzati del browser

<label>

Le etichette: descrivono ogni controllo

<textarea>

Testo su più righe (messaggi, commenti)

<select>

Menu a tendina per scelta tra opzioni

<fieldset> + <legend>

Gruppo...

I raggruppamenti: organizzano sezioni correlate

<button>

I pulsanti: avviano l'invio

In questa lezione esploreremo ognuno di questi pezzi in dettaglio, costruendo form sempre più completi.

Il Contenitore <form>

Il tag <form> è il contenitore per tutti i controlli di un modulo. Due attributi fondamentali:

  • action: l'URL di destinazione dei dati (dove inviarli). Se omesso, invia alla stessa pagina (sconsigliato).
  • method: come inviare i dati. Due valori:
    • GET (default): dati visibili nell'URL. Come una cartolina: tutti possono leggere. Va bene per ricerche e filtri.
    • POST: dati nel corpo della richiesta, non visibili nell'URL. Come una busta chiusa. Obbligatorio per dati sensibili.
<form action="/registrazione" method="POST">
  <!-- Qui andranno i controlli -->
</form>

Regola: è severamente vietato annidare un <form> dentro un altro <form>.

method:

Risposta del server

Premete "Invia" per vedere cosa riceve il server.

Se non specificate method, il browser usa GET di default. Ricordatevi di scrivere method="POST" quando servono dati non visibili nell'URL!

Provate voi!

Scrivete il Vostro Primo Form

Aprite CodePen e scrivete un form da zero. Provate a inviarlo e osservate cosa succede!

Codice di partenza

<!-- Scrivete un form che invia i dati all'endpoint di test -->
<form action="https://guida-form.pages.dev/api/echo" method="GET" target="_blank">

  <p>Il vostro nome: <input type="text" name="nome"></p>

  <button type="submit">Invia</button>
</form>

Cosa esplorare

  1. Inviate il form: si apre una nuova pagina che mostra i dati ricevuti dal server. Cosa vedete?
  2. Guardate la barra degli indirizzi della pagina di risposta: il vostro nome è visibile nell'URL?
  3. Tornate indietro e cambiate method="GET" in method="POST". Inviate di nuovo. L'URL è diverso?
  4. Provate ad aggiungere un secondo campo (es. <input type="text" name="cognome">) e inviate: cosa cambia nella risposta?
  5. Cosa succede se togliete name="nome" dall'input e inviate?

Etichette e Primi Controlli

Il controllo più comune è <input type="text">: un campo per testo a riga singola. È un tag "vuoto" (non ha tag di chiusura).

Ma un input da solo non basta: l'utente non sa cosa scriverci! Serve un'etichetta: il tag <label>.

Collegare label e input con for/id:

  1. Si dà un id univoco all'input: <input id="nome_utente">
  2. Si assegna for alla label con lo stesso valore: <label for="nome_utente">

Benefici:

  • Accessibilità: lo screen reader legge l'etichetta quando l'utente raggiunge il campo
  • Usabilità: cliccando sulla label, il cursore va nel campo (area cliccabile più grande)
<label for="nome_utente">Nome Utente:</label>
<input type="text" id="nome_utente">

Senza label

Cosa ci scrivo?

Label non collegata

Click sulla label: non succede nulla

Label collegata

Click sulla label: il cursore va nel campo!

Ogni controllo interattivo in un form (tranne i pulsanti) deve avere una <label> associata con for/id. Non usate mai solo del testo vicino all'input senza collegarlo!

Come Viaggiano i Dati

Tre concetti fondamentali per capire come i form inviano informazioni:

1. name — il badge identificativo per il server

L'attributo name identifica il dato per il server. Come un badge identificativo: senza badge, il server ignora il campo. Il name è l'etichetta per il server; il <label> è l'etichetta per l'utente.

2. value — il valore associato

Per i campi di testo, il value è ciò che l'utente digita. I dati viaggiano come coppie name=value (es. username=Mario).

3. placeholder — il testo fantasma

Testo suggerimento nel campo vuoto, come il testo grigio scritto a matita in un modulo cartaceo: scompare quando si inizia a scrivere. NON sostituisce la <label>.

4. <button type="submit">

Il pulsante che avvia la raccolta e l'invio di tutte le coppie name=value.

<form action="/processa" method="POST">
  <label for="user">Nome Utente:</label>
  <input type="text" id="user" name="username" placeholder="mario_rossi">

  <button type="submit">Invia</button>
</form>
<!-- Se l'utente scrive "Mario", il server riceve: username=Mario -->

Dati del form (coppie name=value)

namevalueinviato?

Ricordate: id collega la label all'input (per l'utente). name identifica il dato per il server. Sono due cose diverse e servono entrambe!

Attenzione ai <button> nei form!

Un <button> dentro un <form> senza attributo type viene trattato dal browser come type="submit" di default. Questo significa che cliccandolo si invia il form! Se volete un pulsante che non invii (es. per toggle o altre azioni), dovete specificare type="button".

Provate voi!

Costruite un Form di Contatto

Aprite CodePen e mettete insieme tutto quello che avete imparato fin qui: form, label, input, name, e submit.

Codice di partenza

<form action="https://guida-form.pages.dev/api/echo" method="POST" target="_blank">

  <!-- 1. Aggiungete un campo "Nome" con label, id, e name -->

  <!-- 2. Aggiungete un campo "Email" con label, id, e name -->

  <!-- 3. Aggiungete il pulsante di invio -->

</form>

Cosa esplorare

  1. Create i campi seguendo i commenti. Ogni campo ha bisogno di <label for="...">, <input id="..." name="...">, e un placeholder
  2. Inviate il form: la risposta mostra tutte le coppie name=value?
  3. Cosa succede se dimenticate il name su un campo? Appare nella risposta?
  4. Provate a cliccare sul testo della label: il cursore va nel campo?
  5. Cambiate method da POST a GET: vedete i dati nell'URL della risposta?

Campi Speciali: Password e Email

Oltre a type="text", esistono tipi di input specializzati che il browser gestisce in modo diverso:

type="password":

  • Maschera il testo con pallini (••••) — protezione visiva da sguardi indiscreti
  • Attenzione: è solo una maschera visiva! La password viaggia come testo normale nella richiesta

type="email":

  • Il browser verifica che il testo assomigli a un'email (contiene @, dominio, ecc.)
  • Su mobile, mostra una tastiera ottimizzata con @ e . subito accessibili
  • Se il formato è sbagliato, blocca l'invio con un messaggio di errore
<input type="password" id="pwd" name="password">
<input type="email" id="email" name="user_email">

type="text"

Nessuna validazione, nessuna maschera

type="password"

Mostra pallini ••••

type="email"

Scrivete "ciao" e premete Invia ↓

Il tipo password protegge dagli sguardi, non dalla trasmissione. Per la sicurezza della trasmissione serve HTTPS (il lucchetto nel browser). Il tipo email offre una validazione base gratuita, molto utile!

Testo su Più Righe: <textarea>

Per messaggi, commenti o descrizioni lunghe, <input> non basta: serve <textarea>.

Differenze strutturali da <input>:

  • Ha tag di apertura e chiusura: <textarea>...</textarea>
  • Il testo predefinito va tra i tag (non nell'attributo value)
  • Qualsiasi contenuto tra i tag viene trattato come testo semplice

Attributi per dimensioni suggerite:

  • rows: numero di righe visibili (altezza iniziale)
  • cols: numero di caratteri visibili (larghezza iniziale)
  • Oggi si preferisce il CSS per le dimensioni reali
  • Molti browser permettono il ridimensionamento manuale (gestibile con CSS resize)
  • placeholder funziona anche qui
<label for="messaggio">Il vostro messaggio:</label>
<textarea id="messaggio" name="user_message" rows="5" cols="40"
          placeholder="Scrivete qui il messaggio..."></textarea>
rows: 4
cols: 40

Attenzione agli spazi! Se scrivete <textarea> testo </textarea> con spazi extra, quegli spazi appariranno nel campo. Il contenuto tra i tag è letterale.

Provate voi!

Sperimentate con i Tipi di Input

Aprite CodePen e arricchite il vostro form con campi password, email e textarea.

Codice di partenza

<form action="https://guida-form.pages.dev/api/echo" method="POST" target="_blank">

  <p>
    <label for="nome">Nome:</label>
    <input type="text" id="nome" name="username" placeholder="Mario Rossi">
  </p>
  <p>
    <label for="email">Email:</label>
    <!-- Cambiate il type qui sotto: provate "email" -->
    <input type="text" id="email" name="user_email" placeholder="mario@example.com">
  </p>
  <p>
    <label for="pwd">Password:</label>
    <!-- Cambiate il type qui sotto: provate "password" -->
    <input type="text" id="pwd" name="user_pwd">
  </p>
  <p>
    <label for="msg">Messaggio:</label><br>
    <textarea id="msg" name="user_message" rows="4" cols="40"></textarea>
  </p>

  <button type="submit">Invia</button>
</form>

Cosa esplorare

  1. Cambiate il tipo del campo email in type="email". Provate a scrivere "ciao" e inviare: cosa succede?
  2. Cambiate il tipo del campo password in type="password". I caratteri vengono mascherati?
  3. Scrivete un messaggio lungo nella textarea. Provate a ridimensionarla trascinando l'angolo
  4. Inviate il form: tutti i campi appaiono nella risposta con le coppie name=value?
  5. Provate a cambiare il valore di rows della textarea: l'altezza iniziale cambia?

Raggruppare con <fieldset> e <legend>

Quando i form crescono, servono raggruppamenti logici. Come i cassetti di un armadietto: ogni cassetto contiene documenti correlati, con un'etichetta sul fronte.

  • <fieldset>: il cassetto. Raggruppa controlli correlati. Il browser disegna un bordo attorno.
  • <legend>: l'etichetta del cassetto. Deve essere il primo elemento figlio dentro <fieldset>.

Benefici:

  • Chiarezza visiva: il form è diviso in sezioni
  • Accessibilità: lo screen reader annuncia la <legend> prima di ogni campo nel gruppo
<fieldset>
  <legend>Dati Anagrafici</legend>
  <p>
    <label for="nome">Nome:</label>
    <input type="text" id="nome" name="nome">
  </p>
  <p>
    <label for="cognome">Cognome:</label>
    <input type="text" id="cognome" name="cognome">
  </p>
</fieldset>

Scelta Singola: Radio Button

Quando l'utente deve scegliere una sola opzione da un gruppo (esclusiva), si usa <input type="radio">. Come i canali di una vecchia TV: solo uno alla volta.

Regole fondamentali:

  • Tutti i radio dello stesso gruppo devono avere lo stesso name: è il name condiviso che li raggruppa
  • Ogni radio deve avere un value diverso: è il valore inviato al server
  • checked pre-seleziona un'opzione (solo una per gruppo)
  • Una volta selezionato, un radio non può essere deselezionato senza JavaScript
  • Ogni radio ha la propria <label> (con for/id)
  • Vanno sempre dentro <fieldset>/<legend> per l'accessibilità
<fieldset>
  <legend>Dimensione Bevanda</legend>
  <p>
    <input type="radio" id="piccolo" name="dimensione" value="s">
    <label for="piccolo">Piccolo</label>
  </p>
  <p>
    <input type="radio" id="medio" name="dimensione" value="m">
    <label for="medio">Medio</label>
  </p>
  <p>
    <input type="radio" id="grande" name="dimensione" value="l" checked>
    <label for="grande">Grande</label>
  </p>
</fieldset>
<!-- Se scelgo "Medio", il server riceve: dimensione=m -->
Dimensione Bevanda

Dati del form (coppie name=value)

namevalueinviato?
Il server riceve:

Scelta Multipla: Checkbox

Quando l'utente può scegliere zero, una o più opzioni (non esclusive), si usa <input type="checkbox">. Come una lista della spesa: spunti quello che vuoi.

Gestione del name:

  • Opzioni correlate (stessa domanda, es. "interessi"): stesso name, con [] alla fine → name="interessi[]"
  • Opzioni indipendenti (es. "accetto termini", "newsletter"): name diversi

value è fondamentale! Se manca, il browser invia un inutile "on". Specificatelo sempre.

Solo le checkbox selezionate vengono inviate. checked pre-seleziona (più di una possibile).

<!-- Stesso name per opzioni correlate -->
<fieldset>
  <legend>Interessi</legend>
  <input type="checkbox" id="sport" name="interessi[]" value="sport">
  <label for="sport">Sport</label>
  <input type="checkbox" id="musica" name="interessi[]" value="musica" checked>
  <label for="musica">Musica</label>
</fieldset>

<!-- Name diversi per opzioni indipendenti -->
<input type="checkbox" id="terms" name="accetta_termini" value="si">
<label for="terms">Accetto i termini</label>

<!-- Senza value: il browser invia "on" -->
<input type="checkbox" id="test" name="test_senza_value">
<label for="test">Test senza value</label>
Interessi

Dati del form (coppie name=value)

namevalueinviato?
Il server riceve:
Provate voi!

Sperimentate con Radio e Checkbox

Aprite CodePen e create una sezione sondaggio con radio button e checkbox.

Codice di partenza

<form action="https://guida-form.pages.dev/api/echo" method="GET" target="_blank">

  <!-- Radio: scelta singola -->
  <fieldset>
    <legend>Qual è il vostro linguaggio preferito?</legend>
    <p>
      <input type="radio" id="html" name="linguaggio" value="html">
      <label for="html">HTML</label>
    </p>
    <p>
      <input type="radio" id="css" name="linguaggio" value="css">
      <label for="css">CSS</label>
    </p>
    <!-- Aggiungete un'altra opzione "JavaScript" -->
  </fieldset>

  <!-- Checkbox: scelta multipla -->
  <fieldset>
    <legend>Quali strumenti usate? (più risposte)</legend>
    <!-- Aggiungete 3 checkbox con name="strumenti[]" -->
    <!-- Suggerimento: value="vscode", "figma", "github" -->
  </fieldset>

  <button type="submit">Invia Sondaggio</button>
</form>

Cosa esplorare

  1. Completate i radio e le checkbox seguendo i commenti
  2. Inviate con GET: osservate l'URL. I radio appaiono come linguaggio=valore, le checkbox come strumenti[]=valore
  3. Cosa succede se date name diversi ai radio? Si possono selezionare tutti?
  4. Provate a togliere il value da una checkbox: cosa appare nella risposta?
  5. Aggiungete checked a una delle opzioni radio e a due checkbox: sono già selezionate al caricamento?
Esercizio

Modulo di Registrazione

Obiettivo

Create un form di registrazione completo che usa tutti gli elementi visti nella Parte 1.

Dati Personali

Preferenze

Ruolo:

Interessi:

Indicazioni

  1. Il form deve inviare i dati in POST all'endpoint di test, aprendosi in una nuova scheda
  2. La sezione "Dati Personali" raccoglie tre informazioni: come pensate di raccoglierle in modo che il browser possa aiutarvi con la validazione e la privacy?
  3. Ogni campo deve essere accessibile: cosa serve perché uno screen reader sappia descriverlo all'utente?
  4. La sezione "Preferenze" ha due domande diverse: una ammette una sola risposta, l'altra più di una. Quali tipi di input corrispondono a questi due comportamenti?
  5. Pensate ai name: il server deve poter distinguere ogni dato ricevuto. Per le opzioni dello stesso gruppo, come si indicano?
  6. Non dimenticate un modo per inviare il tutto!

Codice di partenza

<form action="https://guida-form.pages.dev/api/echo" method="POST" target="_blank">

  <fieldset>
    <legend>Dati Personali</legend>
    <!-- Aggiungete: nome (text), email (email), password (password) -->
    <!-- Ogni campo: label + input con id, name, placeholder -->
  </fieldset>

  <fieldset>
    <legend>Preferenze</legend>
    <!-- Aggiungete: 3 radio "Ruolo" con name="ruolo" -->
    <!-- Aggiungete: 2 checkbox "Interessi" con name="interessi[]" -->
  </fieldset>

  <!-- Aggiungete il pulsante submit -->
</form>
Mostra Soluzione
<form action="https://guida-form.pages.dev/api/echo" method="POST" target="_blank">

  <fieldset>
    <legend>Dati Personali</legend>
    <!-- Aggiungete: nome (text), email (email), password (password) -->
    <p>
      <label for="nome">Nome:</label>
      <input type="text" id="nome" name="nome" placeholder="Mario Rossi">
    </p>
    
    <p>
      <label for="email">Email:</label>
      <input type="email" id="email" name="email" placeholder="mario@example.com">
    </p>
    
    <p>
      <label for="password">Password:</label>
      <input type="password" id="password" name="password">
    </p>
    
    <!-- Ogni campo: label + input con id, name, placeholder -->
  </fieldset>

  <fieldset>
    <legend>Preferenze</legend>
    
    <!-- Aggiungete: 3 radio "Ruolo" con name="ruolo" -->
    <fieldset>
      <legend>Ruolo</legend>
      <p>
        <input type="radio" id="designer" name="ruolo" value="designer">
        <label for="designer">Designer</label>
      </p>
      
      <p>
        <input type="radio" id="developer" name="ruolo" value="developer">
        <label for="developer">Developer</label>
      </p>
      
      <p>
        <input type="radio" id="entrambi" name="ruolo" value="entrambi">
        <label for="entrambi">Entrambi</label>
      </p>
    </fieldset>
    
    <!-- Aggiungete: 2 checkbox "Interessi" con name="interessi[]" -->
    <fieldset>
      <legend>Interessi</legend>
      <p>
        <input type="checkbox" id="frontend" name="interessi[]" value="frontend">
        <label for="frontend">Frontend</label>
      </p>
      <p>
        <input type="checkbox" id="backend" name="interessi[]" value="backend">
        <label for="backend">Backend</label>
      </p>
    </fieldset>
  </fieldset>

  <!-- Aggiungete il pulsante submit -->
  <button type="submit">Registrati</button>
</form>
          

Riepilogo

Elemento / AttributoScopo
<form>Contenitore del modulo
actionURL di destinazione dei dati
methodMetodo di invio: GET (URL) o POST (corpo)
<input type="text">Campo testo a riga singola
<input type="password">Campo con testo mascherato
<input type="email">Campo email con validazione base
<textarea>Campo testo multi-riga
<label for="...">Etichetta collegata a un controllo (via id)
nameIdentificativo del dato per il server
valueValore del dato inviato
placeholderSuggerimento nel campo vuoto (non sostituisce label)
<button type="submit">Pulsante per inviare il form
<fieldset> + <legend>Raggruppamento logico con titolo
<input type="radio">Scelta singola (name condiviso, value unico)
<input type="checkbox">Scelta multipla (value obbligatorio!)
checkedPre-selezione per radio/checkbox

Menu a Tendina: <select> e <option>

Per scegliere da una lista predefinita di opzioni, si usa il menu a tendina (dropdown) con <select> e <option>.

  • <select>: il contenitore, con name (per l'invio) e id (per la label)
  • <option>: ogni voce selezionabile. Il testo tra i tag è ciò che l'utente vede; l'attributo value è ciò che viene inviato al server

Specificare sempre value: se manca, il browser invia il testo interno dell'opzione.

<label for="nazione">Nazione:</label>
<select id="nazione" name="user_nation">
  <option value="it">Italia</option>
  <option value="fr">Francia</option>
  <option value="de">Germania</option>
</select>

Risposta del server

Premete "Invia" per vedere quale valore viene inviato.

Opzioni Predefinite e Segnaposto

Attributi per controllare il comportamento delle opzioni:

  • selected: opzione predefinita al caricamento della pagina
  • disabled: opzione visibile ma non selezionabile (appare grigia)

Pattern segnaposto (best practice):

<option value="" disabled selected>-- Selezionate un paese --</option>

Con value="" + disabled + selected: appare come scelta iniziale, obbliga l'utente a scegliere attivamente. Se il <select> ha required, questa opzione non sarà considerata valida.

<label for="paese">Paese di Residenza:</label>
<select id="paese" name="user_country">
  <option value="" disabled selected>-- Selezionate un paese --</option>
  <option value="it">Italia</option>
  <option value="fr">Francia</option>
  <option value="es" disabled>Spagna (Non disponibile)</option>
</select>

Raggruppare e Selezione Multipla

<optgroup> per raggruppare opzioni sotto un titolo non selezionabile:

  • Attributo label obbligatorio (il titolo del gruppo)
<select id="bevanda" name="drink">
  <optgroup label="Bevande Calde">
    <option value="the">Tè</option>
    <option value="caffe">Caffè</option>
  </optgroup>
  <optgroup label="Bevande Fredde">
    <option value="acqua">Acqua</option>
    <option value="succo">Succo</option>
  </optgroup>
</select>

multiple per selezione multipla:

  • Trasforma la tendina in una lista scrollabile
  • L'utente seleziona più voci con Ctrl/Cmd+Click
  • size suggerisce quante righe mostrare
  • Il server riceve più valori per lo stesso name
<select id="ingredienti" name="pizza[]" multiple size="5">
  <option value="pomodoro">Pomodoro</option>
  <option value="mozzarella">Mozzarella</option>
  <option value="basilico">Basilico</option>
  <option value="funghi">Funghi</option>
  <option value="prosciutto">Prosciutto</option>
</select>

Provate voi!

Costruite Menu a Tendina

Aprite CodePen e create select con optgroup e provate la selezione multipla.

Codice di partenza

<form action="https://guida-form.pages.dev/api/echo" method="GET" target="_blank">
  <p>
    <label for="orario">Orario Preferito:</label>
    <select id="orario" name="pref_hour">
      <option value="" disabled selected>-- Selezionate --</option>
      <!-- Aggiungete optgroup "Mattina" con opzioni 9:00, 10:00, 11:00 -->
      <!-- Aggiungete optgroup "Pomeriggio" con opzioni 14:00, 15:00, 16:00 -->
    </select>
  </p>

  <button type="submit">Invia</button>
</form>

Cosa esplorare

  1. Completate gli optgroup con le opzioni. Ogni option ha un value significativo (es. "09:00")
  2. Inviate: il value selezionato appare nell'URL?
  3. Aggiungete multiple al select: come cambia l'aspetto?
  4. Con multiple, provate a selezionare più voci (Ctrl/Cmd+Click). Inviate: appaiono tutti i valori?
  5. Aggiungete size="4" con multiple: quante righe vedete?

Input Numerici

<input type="number"> per valori numerici:

  • Mostra spinner (+/-) su desktop, tastiera numerica su mobile
  • Attributi: min, max, step, value, placeholder

Attenzione a step

  • Default step="1": accetta solo numeri interi. Provare a scrivere "3.5" dà errore!
  • step="0.01": accetta centesimi (utile per prezzi)
  • step="0.1": accetta un decimale
  • step="any": qualsiasi decimale senza limiti di precisione
<input type="number" name="qty" min="1" max="10" step="1" value="1">
<input type="number" name="price" min="0" step="0.01" placeholder="Es. 19.99">
<input type="number" name="measure" step="any">
min: 0
max: 100
step:

Lo Slider e <output>

<input type="range"> per selezione approssimativa con cursore:

  • Essenziale specificare min, max, step
  • Problema principale: non mostra il valore numerico selezionato!

<output> risolve: elemento semantico per mostrare risultati. Si collega con for (punta all'id del range).

Con solo HTML, il valore non si aggiorna muovendo lo slider. Serve JavaScript.

<label for="soddisfazione">Soddisfazione (1-5):</label>
<input type="range" id="soddisfazione" name="satisfaction"
       min="1" max="5" step="1" value="3">
<output for="soddisfazione">3</output>
3

Provate a muovere lo slider in modalità "Senza JS": l'output resta fisso! Poi attivate "Con JS" per vedere la differenza. Questo è un caso dove JavaScript fa una vera differenza nell'usabilità.

Provate voi!

Sperimentate con i Valori Numerici

Aprite CodePen e provate i controlli numerici.

Codice di partenza

<form action="https://guida-form.pages.dev/api/echo" method="GET" target="_blank">
  <p>
    <label for="qty">Quantità (1-10):</label>
    <input type="number" id="qty" name="quantita" min="1" max="10" step="1" value="1">
  </p>
  <p>
    <label for="budget">Budget:</label>
    <input type="range" id="budget" name="budget" min="0" max="1000" step="50" value="500">
    <output for="budget">500</output>
  </p>

  <button type="submit">Invia</button>
</form>

Cosa esplorare

  1. Provate a scrivere un numero decimale (3.5) nel campo quantità: cosa succede?
  2. Cambiate step="1" in step="any" e riprovate con un decimale
  3. Muovete lo slider budget: l'output si aggiorna? (No, serve JS!)
  4. Inviate il form: il valore del range appare nella risposta?
  5. Cambiate step="50" del range in step="100": lo slider "salta" diversamente?

Date e Orari

Gestire date e orari è complicato: 10/10, Oct 10, 10 Ottobre... HTML semplifica con input specializzati che attivano selettori nativi e inviano formati standardizzati.

TipoSelezionaFormato inviato
dateAnno, Mese, GiornoAAAA-MM-GG
timeOraHH:MM
datetime-localData + OraAAAA-MM-GGTHH:MM
monthAnno, MeseAAAA-MM
weekAnno, SettimanaAAAA-WNN

Vincoli con min, max, step:

  • min/max: limite date/ore selezionabili (formato coerente col tipo)
  • step per time: in secondi (900 = 15 min, 1800 = 30 min)
  • step per date: in giorni (7 = stesso giorno della settimana)
<input type="date" name="event_date">
<input type="time" name="start_time" min="07:00" max="18:00" step="1800">
<input type="datetime-local" name="arrival"
       min="2026-01-01T00:00" max="2026-12-31T23:59">

Risposta del server

Premete "Invia" per vedere i formati standardizzati.

Input Specializzati: search, tel, url

TipoVantaggiValidazione?
searchSemantica corretta, possibile X per cancellare, ricerche recentiNo
telTastiera telefonica su mobileNo! Accetta qualsiasi testo
urlTastiera con / e . su mobileSì: richiede schema (http/https)
<input type="search" name="query" placeholder="Parola chiave...">
<input type="tel" name="phone" placeholder="+39 123 456 7890">
<input type="url" name="website" placeholder="https://esempio.com">

type="search"

Possibile icona X per cancellare

type="tel"

Scrivete "ciao": nessun errore!

type="url"

Scrivete "ciao" e provate a inviare: errore!

type="tel" NON valida il formato! Per controllare che sia un numero di telefono valido, servono pattern o controlli server-side.

Colore e Campo Nascosto

type="color": apre il selettore colori nativo del sistema operativo. Il valore è sempre una stringa esadecimale #rrggbb minuscola.

<input type="color" name="fav_color" value="#0000ff">

type="hidden": campo completamente invisibile all'utente, ma partecipa all'invio. Deve avere name e value nel codice. Non necessita <label>.

Casi d'uso: ID utente, token di sicurezza, ID prodotto, timestamp, versione del form.

<input type="hidden" name="user_id" value="12345">
<input type="hidden" name="csrf_token" value="aBcDeF">

Dati del form (coppie name=value)

namevalueinviato?
Provate voi!

Sperimentate con gli Input Avanzati

Aprite CodePen e provate i controlli avanzati: date, colore e campo nascosto.

Codice di partenza

<form action="https://guida-form.pages.dev/api/echo" method="GET" target="_blank">
  <p>
    <label for="data">Data Evento:</label>
    <input type="date" id="data" name="event_date">
  </p>
  <p>
    <label for="colore">Colore Tema:</label>
    <input type="color" id="colore" name="theme_color" value="#3b82f6">
  </p>
  <input type="hidden" name="form_version" value="2.0">

  <button type="submit">Invia</button>
</form>

Cosa esplorare

  1. Cliccate sul campo data: appare un calendario? Inviate: in che formato arriva la data?
  2. Cliccate sul selettore colore: scegliete un colore e inviate. Che formato ha il valore?
  3. Guardate la risposta: c'è anche il campo hidden form_version=2.0?
  4. Aggiungete min al campo data (es. min="2026-01-01"): potete selezionare date precedenti?
Esercizio

Form di Prenotazione

Obiettivo

Create un form di prenotazione che combina i controlli della Parte 2: select con optgroup, date, number, range e hidden.

200

Indicazioni

  1. Guardate la preview qui sopra: è il risultato che dovete ottenere. Il codice di partenza ha già la struttura — completate i pezzi mancanti
  2. Il menu "Tipo Camera" ha due categorie: Standard (Singola, Doppia) e Premium (Suite, Deluxe). Quale elemento HTML raggruppa le opzioni sotto un'etichetta? Come si impedisce che il segnaposto iniziale venga inviato?
  3. Le date di check-in e check-out sono già nel codice. Controllate che tipo di input servono per far apparire il selettore calendario
  4. Il campo "Ospiti" accetta da 1 a 6 persone, solo numeri interi. Quale tipo di input offre gli spinner +/- e impedisce valori fuori range?
  5. Il "Budget" va da 50€ a 500€ a scatti di 50€. Quale tipo di input mostra un cursore? Come si fa a mostrare il valore selezionato accanto ad esso?
  6. Il form deve portarsi dietro un codice offerta "SUMMER2026" che l'utente non vede né modifica. Quale tipo di input è pensato per dati invisibili?
  7. Inviate e controllate la risposta del server: compaiono tutti i campi? Se ne manca qualcuno, cosa potrebbe essere sfuggito?

Codice di partenza

<form action="https://guida-form.pages.dev/api/echo" method="POST" target="_blank">

  <p>
    <label for="pren-camera">Tipo Camera:</label>
    <select id="pren-camera" name="room_type">
      <!-- Segnaposto + optgroup Standard + optgroup Premium -->
    </select>
  </p>

  <p>
    <label for="pren-checkin">Check-in:</label>
    <input type="date" id="pren-checkin" name="checkin">
    <label for="pren-checkout">Check-out:</label>
    <input type="date" id="pren-checkout" name="checkout">
  </p>

  <p>
    <label for="pren-ospiti">Ospiti:</label>
    <!-- Completate questo input: name="guests" -->
    <input id="pren-ospiti">
  </p>

  <p>
    <label for="pren-budget">Budget:</label>
    <!-- Completate questo input: name="budget" -->
    <input id="pren-budget">
  </p>

  <!-- Aggiungete un campo: name="offerta_id" -->

  <button type="submit">Prenota</button>
</form>
Mostra Soluzione
<form action="https://guida-form.pages.dev/api/echo" method="POST" target="_blank">

  <p>
    <label for="pren-camera">Tipo Camera:</label>
    <select id="pren-camera" name="room_type">
      <option value="" disabled selected>-- Selezionate --</option>
      <optgroup label="Standard">
        <option value="single">Singola</option>
        <option value="double">Doppia</option>
      </optgroup>
      <optgroup label="Premium">
        <option value="suite">Suite</option>
        <option value="deluxe">Deluxe</option>
      </optgroup>
    </select>
  </p>

  <p>
    <label for="pren-checkin">Check-in:</label>
    <input type="date" id="pren-checkin" name="checkin">
    <label for="pren-checkout">Check-out:</label>
    <input type="date" id="pren-checkout" name="checkout">
  </p>

  <p>
    <label for="pren-ospiti">Ospiti:</label>
    <input type="number" id="pren-ospiti" name="guests" min="1" max="6" step="1" value="1">
  </p>

  <p>
    <label for="pren-budget">Budget (€):</label>
    <input type="range" id="pren-budget" name="budget" min="50" max="500" step="50" value="200">
    <output for="pren-budget">200</output>
  </p>

  <input type="hidden" name="offerta_id" value="SUMMER2026">

  <button type="submit">Prenota</button>
</form>

Riepilogo

Elemento / AttributoScopo
<select> + <option>Menu a tendina (dropdown)
value su optionValore inviato al server (diverso dal testo visibile)
selectedOpzione predefinita
disabledOpzione non selezionabile
<optgroup label="...">Raggruppa opzioni sotto un titolo
multiple + sizeSelezione multipla con lista visibile
<input type="number">Campo numerico con spinner
min, max, stepVincoli su valori numerici e date
step="any"Accetta qualsiasi decimale
<input type="range">Slider per selezione approssimativa
<output for="...">Mostra valore (si aggiorna con JS)
type="date", time, datetime-localSelettori data/ora nativi
type="search", tel, urlInput semantici (tel non valida!)
type="color"Selettore colore (#rrggbb)
type="hidden"Campo invisibile (dati tecnici)

Validazione Client-Side

HTML integra meccanismi di validazione eseguiti direttamente dal browser (lato client), senza JavaScript. Lo scopo principale è migliorare l'esperienza utente, fornendo feedback immediato su errori di compilazione.

Analogia: il buttafuori e la security

  • Il buttafuori (validazione client-side) controlla all'ingresso: "Hai il documento? Sei maggiorenne?" Aiuta a tenere fuori chi ovviamente non dovrebbe entrare.
  • La security interna (validazione server-side) controlla di nuovo dentro, con più attenzione e strumenti migliori.
  • Servono ENTRAMBI: il buttafuori per comodità (feedback immediato), la security per sicurezza reale.

Client e Server

"Client" = il computer/browser dell'utente (chi compila il form).
"Server" = il computer remoto che riceve i dati (chi li elabora).

Campi Obbligatori: required

L'attributo required su <input>, <select> o <textarea> impedisce l'invio se il campo è vuoto.

Se l'utente prova a inviare:

  1. Il browser blocca l'invio
  2. Mostra un messaggio di errore standard
  3. Sposta il focus sul campo non valido

Best practice: accompagnare i campi required con un indicatore visivo (es. *) nella label.

<label for="nome_req">Nome: <span style="color:red;">*</span></label>
<input type="text" id="nome_req" name="username" required>

Risposta del server

Premete "Invia" per testare la validazione.

IL GRANDE AVVERTIMENTO

La validazione HTML (client-side) aiuta l'utente ma NON È SICUREZZA!

Può essere facilmente aggirata da chiunque.

Principio chiave: "Mai fidarsi ciecamente dell'input dell'utente!"

La validazione sul server (server-side) è SEMPRE necessaria! Il server che riceve i dati DEVE SEMPRE ri-validare ogni singolo dato. È l'unica vera garanzia di sicurezza e integrità.

Pensate alla validazione HTML come al buttafuori che controlla l'età all'ingresso di un locale. Utile, ma un documento falso può ingannarlo. La security interna (il server) deve ricontrollare.

Limiti e Formato

Oltre a required, altri attributi di validazione client-side:

Lunghezza testo:

  • minlength: minimo caratteri richiesti
  • maxlength: massimo caratteri consentiti (spesso blocca la digitazione)

Valori numerici/date (già visti): min, max, step

Formato specifico con pattern:

  • Accetta una Regular Expression (regex): un mini-linguaggio per descrivere schemi di testo
  • Esempio CAP: pattern="[0-9]{5}" = "5 cifre numeriche"
  • Usare title per spiegare il formato richiesto (appare nel messaggio di errore)
  • Non serve impararle nel dettaglio ora, si trovano pronte online
<input type="password" name="pwd" required minlength="8">
<input type="text" name="zip" required pattern="[0-9]{5}"
       title="Il CAP deve contenere 5 cifre.">

Risposta del server

Premete "Invia" per testare la validazione.

Provate voi!

Aggiungete Validazione

Aprite CodePen e aggiungete attributi di validazione a un form esistente.

Codice di partenza

<form action="https://guida-form.pages.dev/api/echo" method="POST" target="_blank">
  <p>I campi con * sono obbligatori.</p>

  <p>
    <label for="nome_v">Nome: *</label>
    <input type="text" id="nome_v" name="nome">
    <!-- Aggiungete: required -->
  </p>
  <p>
    <label for="email_v">Email: *</label>
    <input type="email" id="email_v" name="email">
    <!-- Aggiungete: required -->
  </p>
  <p>
    <label for="pwd_v">Password (min 8 caratteri): *</label>
    <input type="password" id="pwd_v" name="password">
    <!-- Aggiungete: required, minlength="8" -->
  </p>
  <p>
    <label for="cap_v">CAP (5 cifre):</label>
    <input type="text" id="cap_v" name="cap">
    <!-- Aggiungete: pattern="[0-9]{5}" title="Inserite 5 cifre" -->
  </p>

  <button type="submit">Registrati</button>
</form>

Cosa esplorare

  • Aggiungete required ai primi 3 campi. Provate a inviare vuoto: cosa succede?
  • Aggiungete minlength="8" alla password. Scrivete "abc" e inviate: errore?
  • Aggiungete pattern="[0-9]{5}" e title al CAP. Scrivete "abc" e inviate
  • Provate a inviare un'email senza @: il tipo email la blocca?
  • Compilate tutto correttamente e inviate: vedete i dati nella risposta?

Lo Styling dei Form con CSS

Personalizzare l'aspetto dei controlli mantenendo accessibilità e funzionalità

Perché lo Styling dei Form è Diverso

Se avete provato ad applicare CSS a un <input> o a un <select>, vi sarete accorti che non si comportano come un <div> o un <p>. I form element sono storicamente legati al sistema operativo: il browser delega il rendering di molti controlli al SO, che li disegna con il proprio aspetto nativo.

Lo stesso <input type="checkbox"> può apparire completamente diverso su Chrome, Safari, Firefox, e su Windows rispetto a macOS.

Non tutti i controlli sono ugualmente stilizzabili. Tre categorie:

  • Facili (rispondono bene al CSS): <input type="text">, <textarea>, <button>, <label>
  • Difficili (richiedono tecniche specifiche): <input type="checkbox">, <input type="radio">
  • Molto difficili (storicamente impossibili senza JS): <select>, <input type="date">, <input type="range">, <input type="file">

Altro problema: i form element non ereditano font-family e font-size dal genitore come la maggior parte degli elementi HTML. Se impostate un font sulla pagina, i vostri <input> e <button> continueranno a usare il font di default del browser. Va corretto manualmente.

Nelle prossime slide vedrete come riprendere il controllo sull'aspetto dei form, partendo da un reset base fino ad arrivare a checkbox e select completamente personalizzati.

La Proprietà appearance e il Reset Base

Il primo strumento per riprendere il controllo è la proprietà CSS appearance. Impostando appearance: none, dite al browser: "Non disegnare questo controllo con lo stile nativo — lascia che sia io a decidere l'aspetto con il mio CSS".

Oltre ad appearance, serve un reset base che risolva i problemi di ereditarietà dei font e del dimensionamento:

/* Reset base per i form element */
button,
input,
select,
textarea {
  font-family: inherit;   /* Eredita il font dalla pagina */
  font-size: 16px;        /* Dimensione leggibile, evita zoom su mobile */
  box-sizing: border-box; /* Padding e bordo inclusi nella larghezza */
}

La proprietà appearance: none si usa sui singoli controlli quando serve:

/* Rimuove lo stile nativo del browser */
input[type="checkbox"],
button {
  -webkit-appearance: none; /* Per Safari */
  appearance: none;
}

Il selettore input[type="checkbox"] è un selettore per attributo: seleziona tutti gli <input> il cui attributo type ha valore "checkbox". La sintassi è elemento[attributo="valore"].

Demo: Reset CSS On/Off

Nuovi Strumenti CSS per Questa Parte

Prima di procedere, tre concetti CSS che userete per la prima volta in questa parte. Li approfondirete in lezioni dedicate — qui serve solo riconoscere la sintassi.

Pseudo-classi

Selettori che si attivano in base allo stato di un elemento. Ne conoscete già una: :hover. In questa parte incontrerete:

  • :focus — quando l'elemento ha il focus (click o Tab)
  • :checked — quando un checkbox o radio è selezionato
  • :disabled — quando un elemento ha l'attributo disabled
  • :valid, :invalid — quando il valore supera o meno la validazione HTML
  • :hover — passaggio del mouse (la conoscete già)

Sintassi: selettore:nome-pseudo-classe, es. input:focus.

Pseudo-elementi

Elementi "virtuali" generati dal CSS, non presenti nell'HTML. Permettono di aggiungere contenuto decorativo:

  • ::before — elemento fittizio prima del contenuto
  • ::after — elemento fittizio dopo il contenuto
  • ::placeholder — stilizza il testo segnaposto degli input

Sintassi con due punti doppi: selettore::nome-pseudo-elemento, es. input::placeholder. Nota: ::before e ::after richiedono sempre la proprietà content (anche vuota: content: "").

Selettore per attributo

Già visto nella slide precedente: [type="checkbox"] seleziona in base a un attributo HTML. Lo userete combinato con pseudo-classi: input[type="checkbox"]:checked seleziona i checkbox selezionati.

Non preoccupatevi di memorizzare tutto ora — li riconoscerete man mano che li usiamo nelle slide seguenti.

Bordi, Padding e Font: Stilizzare Input e Textarea

Con il reset base applicato, potete personalizzare l'aspetto dei campi di testo. Le proprietà più utili:

  • border — bordo personalizzato al posto di quello nativo
  • padding — spazio interno per rendere il testo più leggibile
  • border-radius — angoli arrotondati
  • background-color — colore di sfondo

Per le textarea, aggiungete resize: vertical — impedisce all'utente di allargare il campo orizzontalmente (rompe il layout), ma permette di allungarlo verticalmente.

/* Input di testo personalizzato */
.campo-custom {
  font-size: 18px;
  font-family: inherit;
  padding: 12px 16px;
  background-color: #f0f4ff;
  border: 2px solid #6366f1;
  border-radius: 12px;
}

/* Textarea: impedisce il resize orizzontale */
textarea.campo-custom {
  resize: vertical;
}
<form>
  <label for="nome-stile">Nome:</label>
  <input type="text" id="nome-stile" name="nome" class="campo-custom">

  <label for="msg-stile">Messaggio:</label>
  <textarea id="msg-stile" name="messaggio" class="campo-custom" rows="4"></textarea>
</form>

Lo Stato :focus — Mai Rimuovere, Sempre Migliorare

Quando un utente clicca su un campo o ci arriva premendo Tab, quell'elemento riceve il focus. Il browser lo segnala con un contorno (outline) — è così che gli utenti che navigano con la tastiera sanno dove si trovano.

Regola importante: Non scrivete mai outline: none senza fornire un'alternativa visiva. Togliere il focus ring senza sostituirlo è un errore grave di accessibilità — gli utenti che navigano con la tastiera non saprebbero più su quale campo si trovano.

La tecnica corretta: cambiate il colore del bordo, aggiungete un box-shadow come "anello" e impostate outline: 3px solid transparent (per la modalità Alto Contrasto di Windows, dove le ombre non sono visibili).

/* Stato focus: anello colorato + bordo evidenziato */
.campo-custom:focus {
  border-color: #3730a3;
  box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.8);
  outline: 3px solid transparent; /* Per Windows High Contrast */
}
<form>
  <label for="email-focus">Email:</label>
  <input type="email" id="email-focus" name="email" class="campo-custom"
         placeholder="vostro@email.it">
</form>

Provate a cliccare sul campo e poi a usare Tab per spostarvi: vedrete l'anello di focus apparire e scomparire.

::placeholder, :disabled e [readonly]

Lo pseudo-elemento ::placeholder permette di stilizzare il testo segnaposto — quel testo grigio che appare nei campi vuoti:

/* Stilizzare il placeholder */
.campo-custom::placeholder {
  color: #8b8ec5;
  font-style: italic;
}

Per i campi disabilitati, la pseudo-classe :disabled si attiva quando l'elemento ha l'attributo HTML disabled:

/* Stile per campi disabilitati */
.campo-custom:disabled {
  background-color: #eee;
  border-color: #ccc;
  color: #999;
  cursor: not-allowed;
}

Per i campi di sola lettura (readonly), il selettore per attributo [readonly]:

/* Stile per campi in sola lettura */
.campo-custom[readonly] {
  background-color: #f9f9f9;
  border-style: dashed;
  color: #666;
}

La differenza tra disabled e readonly: un campo disabled non viene inviato con il form e non può ricevere il focus. Un campo readonly viene inviato ma l'utente non può modificarlo — utile per mostrare valori calcolati o pre-compilati.

<form>
  <label for="email-ph">Email:</label>
  <input type="email" id="email-ph" name="email" class="campo-custom"
         placeholder="es. mario@email.it">

  <label for="codice-ro">Codice ordine:</label>
  <input type="text" id="codice-ro" name="codice" class="campo-custom"
         value="ORD-2024-001" readonly>

  <label for="campo-dis">Campo bloccato:</label>
  <input type="text" id="campo-dis" name="bloccato" class="campo-custom"
         value="Non modificabile" disabled>
</form>

Bottoni Belli e Accessibili

I <button> nativi del browser hanno stili che variano molto tra i sistemi operativi. Per un aspetto uniforme e moderno, si parte da un reset e si ricostruisce lo stile.

/* Stile moderno per il bottone */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 20px;
  min-height: 44px;       /* Area di tocco accessibile (WCAG 2.1) */
  background-color: #3e68ff;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  box-shadow: 0 3px 5px rgba(0, 0, 0, 0.18);
  cursor: pointer;
}

/* Hover, Focus, Active, Disabled */
.btn:hover { background-color: #2952cc; }
.btn:focus {
  outline-style: solid;
  outline-color: transparent;
  box-shadow: 0 0 0 4px #1a3399;
}
.btn:active {
  transform: translateY(1px);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
}
.btn:disabled {
  background-color: #aaa;
  cursor: not-allowed;
  box-shadow: none;
}

Passate il mouse sul bottone, cliccateci, usate Tab per raggiungerlo — ogni stato ha un feedback visivo chiaro.

Confronto: Default vs Personalizzato

Default del browser



Stile variabile tra browser e SO

Con classe .btn



Stile uniforme e accessibile su tutti i browser

Esercizio

Crea una Lista di Note

Obiettivo

Su CodePen, costruite un'app per gestire una lista di note. Un form in alto per aggiungerne di nuove, e ogni nota ha il proprio form con un pulsante per eliminarla. Scrivete sia l'HTML sia il CSS seguendo i commenti nello scheletro di partenza.

Apri il mockup su Figma per vedere il risultato da replicare.

Indicazioni

  1. Il titolo deve essere centrato, in grassetto, con spazio sotto.
  2. Nel form di aggiunta, campo di testo e bottone devono stare sulla stessa riga. Il campo deve occupare tutto lo spazio rimasto.
  3. La lista di note ha larghezza limitata e sta al centro della pagina. Le card si impilano con un po' di spazio tra loro.
  4. Ogni card nota ha uno sfondo grigio chiaro, un bordo sottile e gli elementi affiancati. L'input disabilitato deve essere "invisibile" (trasparente, senza bordo) ma con testo scuro leggibile.
  5. I bottoni devono avere colori decisi (verde per aggiungere, rosso per eliminare), testo bianco e nessun bordo.
  6. Se avete inserito correttamente i campi del form, cliccando "Aggiungi Nota" dovreste essere portati a una nuova pagina con la nota appena aggiunta. Cliccando "Elimina" la nota dovrebbe sparire. Niente JavaScript — è tutto sul server!

Codice di partenza (HTML)

<!-- Titolo della pagina -->

<!-- Form per AGGIUNGERE una nota
     method POST, action: https://guida-forms.pages.dev/api/notes
     classe: ex-note-add-form -->

  <!-- Campo hidden: name="action" value="add" -->
  <!-- Campo hidden: name="note[]" value="Esempio nota"
       (trasporta le note esistenti ad ogni invio) -->
  <!-- Campo di testo: name="nota",
       placeholder "Inserisci una nuova nota", obbligatorio -->
  <!-- Bottone di invio: "Aggiungi Nota" -->

  <!-- ...non vi starete scordando di chiudere il form, vero? -->

<!-- div con classe: ex-note-list -->

  <!-- Form per ELIMINARE una nota
       method POST, stessa action del form sopra
       classe: ex-note-card -->

    <!-- Campo hidden: name="action" value="delete" -->
    <!-- Campo hidden: name="delete_index" value="0"
         (indica quale nota eliminare) -->
    <!-- Campo hidden: name="note[]" value="Esempio nota"
         (stessa tecnica del form sopra) -->
    <!-- Input di testo disabilitato con il testo della nota -->
    <!-- Bottone di invio: "Elimina" -->

  <!-- ...non vi starete scordando di chiudere il form, vero? -->

<!-- ...non vi starete scordando di chiudere il div, vero? -->

Codice di partenza (CSS)

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: system-ui, sans-serif;
  background: #fff;
  padding: 32px 24px;
}
Mostra Soluzione (Flexbox)
<h1>Crea una Lista di Note</h1>

<form method="POST"
      action="https://guida-forms.pages.dev/api/notes"
      class="ex-note-add-form">
  <input type="hidden" name="action" value="add">
  <input type="hidden" name="note[]" value="Esempio nota">
  <input type="text" name="nota"
         placeholder="Inserisci una nuova nota" required>
  <button type="submit">Aggiungi Nota</button>
</form>

<div class="ex-note-list">

  <form method="POST"
        action="https://guida-forms.pages.dev/api/notes"
        class="ex-note-card">
    <input type="hidden" name="action" value="delete">
    <input type="hidden" name="delete_index" value="0">
    <input type="hidden" name="note[]" value="Esempio nota">
    <input type="text" value="Esempio nota" disabled>
    <button type="submit">Elimina</button>
  </form>

</div>
/* Reset */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: system-ui, sans-serif;
  background: #fff;
  padding: 32px 24px;
}

h1 {
  text-align: center;
  font-weight: 700;
  margin-bottom: 24px;
}

/* Form di aggiunta */
.ex-note-add-form {
  display: flex;
  gap: 8px;
  margin-bottom: 32px;
}

.ex-note-add-form input[type="text"] {
  flex: 1;
  padding: 10px 14px;
  border: 1px solid #d4d4d4;
  border-radius: 6px;
  font-size: 1rem;
}

.ex-note-add-form button {
  background: #16a34a;
  color: white;
  border: none;
  border-radius: 6px;
  padding: 10px 20px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.ex-note-add-form button:hover {
  background: #15803d;
}

/* Lista note */
.ex-note-list {
  max-width: 500px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Card nota */
.ex-note-card {
  display: flex;
  align-items: center;
  padding: 8px 12px;
  border: 1px solid #d4d4d4;
  border-radius: 6px;
  background: #f5f5f5;
  gap: 8px;
}

.ex-note-card input[type="text"] {
  flex: 1;
  border: none;
  background: transparent;
  font-size: 1rem;
  font-family: inherit;
  color: #1a1a1a;
  padding: 4px 6px;
}

.ex-note-card button {
  background: #dc2626;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 6px 14px;
  font-weight: 600;
  font-size: 0.85rem;
  cursor: pointer;
}

.ex-note-card button:hover {
  background: #b91c1c;
}
Soluzione alternativa (Grid)
/* Reset */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: system-ui, sans-serif;
  background: #fff;
  padding: 32px 24px;
}

h1 {
  text-align: center;
  font-weight: 700;
  margin-bottom: 24px;
}

/* Form di aggiunta — griglia a 2 colonne */
.ex-note-add-form {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 8px;
  margin-bottom: 32px;
}

.ex-note-add-form input[type="text"] {
  padding: 10px 14px;
  border: 1px solid #d4d4d4;
  border-radius: 6px;
  font-size: 1rem;
}

.ex-note-add-form button {
  background: #16a34a;
  color: white;
  border: none;
  border-radius: 6px;
  padding: 10px 20px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.ex-note-add-form button:hover {
  background: #15803d;
}

/* Lista note — griglia centrata a larghezza limitata */
.ex-note-list {
  display: grid;
  grid-template-columns: minmax(0, 500px);
  justify-content: center;
  gap: 10px;
}

/* Card nota — griglia a 2 colonne */
.ex-note-card {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  padding: 8px 12px;
  border: 1px solid #d4d4d4;
  border-radius: 6px;
  background: #f5f5f5;
  gap: 8px;
}

.ex-note-card input[type="text"] {
  border: none;
  background: transparent;
  font-size: 1rem;
  font-family: inherit;
  color: #1a1a1a;
  padding: 4px 6px;
}

.ex-note-card button {
  background: #dc2626;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 6px 14px;
  font-weight: 600;
  font-size: 0.85rem;
  cursor: pointer;
}

.ex-note-card button:hover {
  background: #b91c1c;
}

Checkbox e Radio — Il Problema del Default

I checkbox e i radio button nativi presentano due problemi:

  1. Aspetto diverso su ogni browser: un checkbox su Chrome non è identico a uno su Safari o Firefox, e su sistemi operativi diversi le differenze sono ancora maggiori.
  2. Non si ridimensionano: modificare font-size non cambia la dimensione del checkbox nativo, che resta piccolo e fuori scala rispetto al design.

La soluzione è un processo in 4 step:

  1. Nascondere lo stile nativo con appearance: none
  2. Ricostruire il box con bordo, dimensioni e colori personalizzati
  3. Creare il segno di spunta con il pseudo-elemento ::before
  4. Aggiungere gli stati (:focus, :disabled, :hover)

Il risultato è un checkbox (o radio) completamente personalizzato, accessibile e coerente su tutti i browser.

Nella prossima slide vedrete tutti e 4 gli step in un'unica demo interattiva dove potete costruire il checkbox passo dopo passo.

Provate voi!

Checkbox Personalizzato in 4 Step

Costruiamo un checkbox personalizzato step by step. Usate i pulsanti per aggiungere un layer di CSS alla volta e osservare il risultato.

Cliccate su uno step per costruire il checkbox progressivamente.

Demo

Checkbox

Radio

CSS Applicato

/* Step 1: Rimuove lo stile nativo */
input[type="checkbox"] {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
}

/* Step 2: Ricostruisce il box */
input[type="checkbox"] {
  background-color: #fff;
  width: 20px;
  height: 20px;
  border: 2px solid currentColor;
  border-radius: 3px;
  display: grid;
  place-content: center;
}

/* Step 3: Spunta con ::before */
/* Qui sarebbe meglio usare background-image
   o qualche tecnica più avanzata */
input[type="checkbox"]::before {
  content: "✔";
  color: #3e68ff;
  font-size: 14px;
  opacity: 0;
}
input[type="checkbox"]:checked::before {
  opacity: 1;
}

/* Step 4: Stati */
input[type="checkbox"]:focus {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}
input[type="checkbox"]:hover {
  border-color: #3e68ff;
}
input[type="checkbox"]:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

Variante Radio: Stessa tecnica con due differenze — border-radius: 50% per il box circolare e un ::before circolare con background-color invece dell'emoji.

<select> — L'Elemento Più Difficile da Stilizzare

Il <select> è stato per anni l'elemento più frustrante per chi voleva personalizzare i form. Il motivo? La lista delle opzioni (il "dropdown") è gestita direttamente dal sistema operativo — il CSS non può raggiungerla. Per questo, molti sviluppatori hanno costruito select falsi con JavaScript, perdendo accessibilità e prestazioni.

Con il CSS classico, si poteva stilizzare solo il "bottone" esterno del select — quello che mostra l'opzione selezionata. La lista restava nativa, con l'aspetto del SO.

Vi mostriamo due approcci: quello tradizionale (funziona ovunque) e quello nuovo (sperimentale, Chrome 135+), che finalmente permette di personalizzare tutto.

Provate voi!

L'Approccio Corrente (Limitato)

Il <select> è l'elemento dei form più difficile da personalizzare. Il bottone si può stilare, ma il dropdown resta nativo del sistema operativo. La tecnica attuale si basa su tre passaggi:

  1. appearance: none per rimuovere la freccia nativa
  2. Un <div> wrapper che fornisce bordo, sfondo e posiziona una freccia custom
  3. CSS Grid per sovrapporre <select> e freccia ::after nella stessa area
select {
  appearance: none;
  background-color: transparent;
  border: none;
  padding: 0 16px 0 0;
  width: 100%;
  font: inherit;
  cursor: inherit;
  outline: none;
}

.select-wrapper {
  border: 2px solid #777;
  border-radius: 4px;
  padding: 8px 12px;
  background-color: #fff;
  display: grid;
  grid-template-areas: "select";
  align-items: center;
}

.select-wrapper select,
.select-wrapper::after {
  grid-area: select;
}

/* Qui sarebbe meglio usare background-image
   o un SVG. Per semplicità usiamo un'emoji. */
.select-wrapper::after {
  content: "▼";
  color: #777;
  font-size: 14px;
  justify-self: end;
}
<label for="paese">Paese:</label>
<div class="select-wrapper">
  <select id="paese" name="paese">
    <option value="" disabled selected>
      -- Selezionate --
    </option>
    <option value="it">Italia</option>
    <option value="fr">Francia</option>
    <option value="de">Germania</option>
  </select>
</div>
Il futuro: appearance: base-select — Chrome 135+ (aprile 2025) introduce una tecnologia sperimentale che permette di personalizzare tutto: bottone, dropdown, opzioni, freccia. Per ora funziona solo su Chrome e non è pronta per la produzione. Per approfondire: The <select> element can now be customized with CSS (Chrome DevRel).

Stilizzare Campi Obbligatori e Opzionali

Ricordate la validazione HTML vista nelle Parti precedenti? Con CSS potete dare feedback visivo immediato sulla natura dei campi.

Due pseudo-classi:

  • :required — si attiva su ogni campo che ha l'attributo required
  • :optional — si attiva su ogni campo che non ha required
/* Campi obbligatori: bordo pieno e più evidente */
input:required,
textarea:required,
select:required {
  border: 2px solid #333;
}

/* Campi opzionali: bordo tratteggiato e più leggero */
input:optional,
textarea:optional,
select:optional {
  border: 2px dashed #999;
}
<form>
  <label for="nome-req">Nome: *</label>
  <input type="text" id="nome-req" name="nome" required
         class="campo-custom">

  <label for="tel-opt">Telefono (opzionale):</label>
  <input type="tel" id="tel-opt" name="telefono"
         class="campo-custom">

  <button type="submit" class="btn">Invia</button>
</form>

Un consiglio: non usate solo il colore per distinguere obbligatorio da opzionale — è problematico per utenti con daltonismo. Affiancate sempre un indicatore testuale, come l'asterisco * nella label o la scritta "(opzionale)".

:invalid vs :user-invalid — La Differenza che Conta

Le pseudo-classi :valid e :invalid si attivano in base alla validazione HTML del campo. Per esempio, un <input type="email" required> è :invalid se è vuoto o contiene un testo che non è un indirizzo email.

C'è un grosso problema: :invalid si attiva subito al caricamento della pagina, prima che l'utente abbia toccato il form!

/* PROBLEMA: bordo rosso SUBITO, anche prima che l'utente scriva! */
input:invalid {
  border: 2px solid red;
}

/* SOLUZIONE: bordo rosso solo DOPO che l'utente ha interagito */
input:user-invalid,
textarea:user-invalid {
  border: 2px solid #d32f2f;
  background-color: #fff5f5;
}

/* Bordo verde quando il valore è corretto, dopo l'interazione */
input:user-valid,
textarea:user-valid {
  border: 2px solid #2e7d32;
  background-color: #f5fff5;
}

:user-valid e :user-invalid sono supportate da tutti i browser moderni (Baseline 2023). Usate sempre :user-invalid al posto di :invalid per la validazione visiva.

Con :invalid (bordo rosso subito!)

Il campo è rosso al caricamento, ancora prima che scriviate!

Con :user-invalid (dopo interazione)

Rosso solo dopo che interagite e uscite dal campo con un valore non valido

accent-color — Il Colore del Brand in Una Riga

Per un cambio di colore rapido, esiste una scorciatoia: la proprietà accent-color. Con una sola riga di CSS, colorate i controlli nativi del browser — checkbox, radio, range slider e progress bar:

/* Una riga per colorare checkbox, radio, range e progress */
:root {
  accent-color: #3e68ff;
}

/* Oppure su singoli elementi */
input[type="checkbox"] {
  accent-color: #3e68ff;
}
input[type="radio"] {
  accent-color: hotpink;
}

Il browser sceglie automaticamente il colore giusto per il segno di spunta (bianco su sfondo scuro, scuro su sfondo chiaro) garantendo il contrasto.

Quando usare accent-color vs appearance: none? Se vi serve solo cambiare il colore dei controlli nativi, accent-color è perfetta — veloce, accessibile, nessun rischio. Se vi serve personalizzare la forma, le dimensioni o aggiungere animazioni, serve la tecnica completa con appearance: none + ::before.

75%

field-sizing: content — Dimensioni Automatiche

Avete mai desiderato che una textarea crescesse automaticamente man mano che l'utente scrive? La proprietà field-sizing: content fa esattamente questo.

textarea {
  field-sizing: content;
  min-height: 56px;    /* Almeno ~3 righe */
  min-width: 200px;
  max-width: 500px;
}

select {
  field-sizing: content;
  min-width: 80px;
}

input {
  field-sizing: content;
  min-width: 100px;
  max-width: 400px;
}

Supporto browser: Chrome 123+, Edge 123+, Safari 26.2+. Firefox non lo supporta ancora. Usatelo come progressive enhancement — i browser che non lo supportano mostreranno i campi a dimensione fissa. Nessun danno.

Riepilogo — Styling dei Form CSS

Proprietà / TecnicaScopo
font-family: inheritI form element ereditano il font della pagina
box-sizing: border-boxDimensioni prevedibili con padding e bordo inclusi
appearance: noneRimuove lo stile nativo del browser
:focus + box-shadowFocus ring accessibile e personalizzato
::placeholderStilizzare il testo segnaposto
:hover, :activeFeedback visivo al passaggio del mouse e al click
:disabled / [readonly]Stile per elementi disabilitati e di sola lettura
appearance: none + ::beforeCheckbox e radio completamente personalizzati
clip-pathCreare forme geometriche (spunta, freccia)
:checkedStilizzare checkbox/radio quando selezionati
appearance: base-selectOpt-in al customizable select (sperimentale)
::picker(select), ::picker-iconPersonalizzare dropdown e freccia del select
:required / :optionalDistinguere campi obbligatori da opzionali
:user-valid / :user-invalidValidazione visiva dopo interazione utente
accent-colorColorare checkbox/radio/range/progress con un colore
field-sizing: contentDimensioni automatiche basate sul contenuto

Ricordate:

  • Partite sempre dal reset (font: inherit, box-sizing)
  • Non togliete mai il focus ring senza un'alternativa
  • Usate :user-invalid al posto di :invalid per la validazione visiva
  • accent-color è la scorciatoia perfetta per un branding rapido
Esercizio

Esercizio Finale — Form di Registrazione Stilizzato

Obiettivo

Su CodePen, trovate un form di registrazione con stili di default del browser. Include: campo nome, campo email obbligatorio, campo password, checkbox "Accetto i termini", select per il paese e bottone di invio. Trasformatelo in un form completamente stilizzato applicando tutte le tecniche viste in questa parte.

Istruzioni

  1. Procedete in ordine: prima il reset base, poi i campi di testo, poi il bottone, poi il checkbox, poi il select, infine la validazione visiva.
  2. Per il reset, quali proprietà garantiscono font uniformi e dimensioni prevedibili?
  3. I campi di testo hanno bisogno di bordo, padding e un focus ring accessibile.
  4. Il bottone deve avere un'area di tocco adeguata e stati interattivi (hover, focus, active).
  5. Il checkbox richiede la tecnica completa in 4 step: nascondere, ricostruire, spunta con ::before, stati.
  6. Per il select, scegliete: wrapper con freccia custom oppure base-select con @supports?
  7. I campi obbligatori devono mostrare feedback dopo l'interazione — quale pseudo-classe è meglio di :invalid?

Codice di partenza (HTML)

<form action="https://guida-forms.pages.dev/api/echo"
      method="POST" target="_blank">
  <label for="ex3-nome">Nome:</label>
  <input type="text" id="ex3-nome" name="nome">

  <label for="ex3-email">Email: *</label>
  <input type="email" id="ex3-email" name="email" required>

  <label for="ex3-pwd">Password: *</label>
  <input type="password" id="ex3-pwd" name="password"
         required minlength="8">

  <div class="inline-option">
    <input type="checkbox" id="ex3-termini" name="termini"
           value="accetto" required>
    <label for="ex3-termini">Accetto i termini e condizioni</label>
  </div>

  <label for="ex3-paese">Paese:</label>
  <div class="select-wrapper">
    <select id="ex3-paese" name="paese">
      <option value="" disabled selected>-- Selezionate --</option>
      <option value="it">Italia</option>
      <option value="fr">Francia</option>
      <option value="de">Germania</option>
      <option value="es">Spagna</option>
    </select>
  </div>

  <button type="submit">Registrati</button>
</form>

Codice di partenza (CSS)

/* Scrivete il vostro CSS qui */
Mostra Soluzione
/* 1. Reset base */
input, textarea, select, button {
  font-family: inherit;
  font-size: 16px;
  box-sizing: border-box;
}

/* 2. Campi di testo */
input[type="text"],
input[type="email"],
input[type="password"] {
  padding: 8px 12px;
  border: 2px solid #8b8a8b;
  border-radius: 4px;
  width: 100%;
}

input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus {
  border-color: #3730a3;
  box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.8);
  outline: 3px solid transparent;
}

/* 3. Bottone */
button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 20px;
  min-height: 44px;
  background-color: #3e68ff;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
}

button:hover { background-color: #2952cc; }
button:focus {
  outline-style: solid;
  outline-color: transparent;
  box-shadow: 0 0 0 4px #1a3399;
}

/* 4. Checkbox personalizzato */
input[type="checkbox"] {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
  background-color: #fff;
  color: currentColor;
  width: 20px;
  height: 20px;
  border: 2px solid currentColor;
  border-radius: 3px;
  display: grid;
  place-content: center;
}

input[type="checkbox"]::before {
  content: "";
  width: 12px;
  height: 12px;
  transform: scale(0);
  box-shadow: inset 16px 16px #3e68ff;
  clip-path: polygon(14% 44%, 0 65%, 50% 100%,
    100% 16%, 80% 0%, 43% 62%);
}

input[type="checkbox"]:checked::before {
  transform: scale(1);
}

/* 5. Select con wrapper */
select {
  appearance: none;
  background-color: transparent;
  border: none;
  width: 100%;
  font-family: inherit;
  font-size: inherit;
  cursor: pointer;
  outline: none;
}

.select-wrapper {
  border: 2px solid #777;
  border-radius: 4px;
  padding: 8px 12px;
  background-color: #fff;
  display: grid;
  grid-template-areas: "select";
  align-items: center;
}

.select-wrapper select,
.select-wrapper::after {
  grid-area: select;
}

/* Per semplicità usiamo un'emoji come freccia.
   In produzione: background-image o SVG. */
.select-wrapper::after {
  content: "▼";
  color: #777;
  font-size: 14px;
  justify-self: end;
}

/* 6. Validazione visiva */
input:user-invalid {
  border: 2px solid #d32f2f;
  background-color: #fff5f5;
}

input:user-valid {
  border: 2px solid #2e7d32;
  background-color: #f5fff5;
}
1 / 57
Indice

Indice delle slide

⌘K