Stati dei pulsanti CSS: hover, focus, active, disabled, outline e pulsanti con icona

Gli stati di un pulsante CSS sono gli stili che mostra quando ci passi sopra (:hover), riceve il focus da tastiera (:focus-visible), viene premuto (:active), è disabilitato (:disabled o aria-disabled) o in caricamento. Ecco un pulsante in ogni stato, con CSS puro e Tailwind da copiare, più pulsanti outline, ghost, di chiusura e con icona che restano accessibili.

Un pulsante ha sei stati da progettare: default, hover, focus-visible, active, disabled e loading. La maggior parte degli snippet online ne mostra uno o due, su pulsanti diversi, con codice diverso. Questa guida usa un solo pulsante dall'inizio alla fine, mostra tutti gli stati uno accanto all'altro e fornisce CSS puro e Tailwind da copiare per ciascuno. Poi tratta le varianti più cercate (outline, ghost, chiusura, icona) e la differenza tra disabled e aria-disabled, che è più di una scelta di stile.

La matrice degli stati: un pulsante, sei stati

Ogni riquadro qui sotto è disegnato in uno stato, così puoi confrontarli a colpo d'occhio. Il pulsante dal vivo sotto la griglia ti permette di forzare uno stato o di provarlo con il tuo mouse e la tua tastiera.

Salva modifiche
Default. Stato di riposo. Il bianco su blue-600 (#2563eb) ha un contrasto di circa 5.2:1.
Salva modifiche
Hover. Puntatore sopra il pulsante. Un tono più scuro; nessun cambiamento di layout.
Salva modifiche
Focus-visible. Raggiunto da tastiera. Un anello di 2px con uno spazio di 2px, così si legge su qualsiasi sfondo.
Salva modifiche
Active. Mentre è premuto. Ancora più scuro e spostato in basso di 1px.
Salva modifiche
Disabled. Non disponibile. Sbiadito, niente hover, niente puntatore.
Salvataggio
Loading. Al lavoro. Spinner più un verbo che dice cosa sta succedendo.
StatoSelettore CSSVariante TailwindCosa lo attiva
Default.btn(nessun prefisso)Niente
Hover.btn:hoverhover:Un puntatore sopra il pulsante. Gli schermi touch non hanno un vero hover.
Focus-visible.btn:focus-visiblefocus-visible:Il focus che il browser decide di mostrare, di solito il focus da tastiera (Tab)
Focus.btn:focusfocus:Qualsiasi focus, compreso un clic del mouse nella maggior parte dei browser
Active.btn:activeactive:Mentre il tasto del mouse, il dito o il tasto Space sono tenuti premuti
Disabled.btn:disableddisabled:L'attributo disabled
Disabled (ARIA).btn[aria-disabled="true"]aria-disabled:L'attributo aria-disabled="true"
Loading.btn[aria-busy="true"]aria-busy:Il tuo script, mentre il lavoro è in corso

Nel CSS puro l'ordine conta quando i selettori hanno la stessa specificità: metti prima :hover, poi :focus-visible, poi :active, così la pressione vince sull'hover. Tailwind ordina le varianti al posto tuo.

Il pulsante completo in CSS puro

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 2.5rem;              /* 40px, above the 24px WCAG minimum */
  padding: 0.625rem 1rem;
  border: 1px solid transparent;    /* keeps size equal to the outline variant */
  border-radius: 0.5rem;
  background: #2563eb;
  color: #fff;
  font: 600 0.875rem/1.25rem system-ui, sans-serif;
  cursor: pointer;
  transition: background-color 150ms, transform 150ms;
}

/* hover only when the button can actually be used */
.btn:hover:not(:disabled, [aria-disabled="true"]) {
  background: #1d4ed8;
}

/* keyboard focus; mouse clicks do not show the ring */
.btn:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

.btn:active:not(:disabled, [aria-disabled="true"]) {
  background: #1e40af;
  transform: translateY(1px);
}

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn[aria-busy="true"] {
  cursor: progress;
}

@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
  .btn:active { transform: none; }
}

Tre dettagli fanno quasi tutto il lavoro. La protezione :not(:disabled, [aria-disabled="true"]) impedisce a un pulsante disabilitato di reagire a hover o pressione, cosa che altrimenti lo fa sembrare cliccabile. Il bordo trasparente mantiene un pulsante pieno della stessa dimensione di uno outline, così cambiare variante non sposta il layout. E :focus-visible al posto di :focus fa sì che chi usa il mouse non veda un anello dopo il clic, mentre chi usa la tastiera lo vede sempre.

Usalo così: <button type="button" class="btn">Save changes</button>. Imposta sempre type: un <button> dentro un modulo ha come valore predefinito type="submit" e invierà il modulo quando viene cliccato.

Lo stesso pulsante in Tailwind

<button type="button"
  class="inline-flex items-center justify-center gap-2 rounded-lg border border-transparent
         bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white transition
         hover:bg-blue-700 active:translate-y-px active:bg-blue-800
         focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600
         disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-blue-600 disabled:active:translate-y-0
         aria-disabled:cursor-not-allowed aria-disabled:opacity-50
         motion-reduce:transition-none">
  Save changes
</button>

Queste classi funzionano in Tailwind 3.4 e 4. Due note sulla v4: hover: si applica solo sui dispositivi che supportano l'hover (@media (hover: hover)), e gli stili base della v4 danno ai pulsanti cursor: default, quindi aggiungi cursor-pointer se vuoi il cursore a mano. Consulta il cheat sheet di Tailwind v4 per gli altri valori predefiniti rinominati, come il ring da 1px.

Focus-visible: lo stato che non puoi saltare

Il criterio WCAG 2.4.7 Focus Visible (livello AA) richiede che il focus da tastiera sia visibile. Rimuovere l'outline del browser con outline: none senza aggiungere nulla al suo posto non lo soddisfa. Linee guida che reggono:

Lo stato di caricamento

Un pulsante in caricamento deve fare quattro cose: mostrare che il lavoro è in corso, bloccare un secondo invio, mantenere il focus da tastiera dov'era e avvisare chi usa uno screen reader quando il lavoro finisce. Impostare disabled durante la richiesta rompe il terzo punto: il pulsante perde il focus e in alcuni browser chi usa la tastiera viene riportato all'inizio della pagina. Usa invece aria-disabled e un controllo nel tuo script.

<button type="submit" class="btn" aria-disabled="true" aria-busy="true">
  <svg class="spinner" viewBox="0 0 24 24" width="16" height="16" fill="none" aria-hidden="true">
    <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-opacity=".3" stroke-width="3"/>
    <path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
  </svg>
  Saving
</button>
<!-- one live region on the page, outside the button -->
<p class="visually-hidden" role="status">Saving your changes</p>
.spinner { animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 2.4s; }  /* slow it, keep a sign of progress */
}
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
form.addEventListener("submit", async (event) => {
  event.preventDefault();
  if (button.getAttribute("aria-disabled") === "true") return; // ignore double clicks
  button.setAttribute("aria-disabled", "true");
  button.setAttribute("aria-busy", "true");
  status.textContent = "Saving your changes";
  try {
    await save(new FormData(form));
    status.textContent = "Saved";
  } catch {
    status.textContent = "Could not save. Try again.";
  } finally {
    button.removeAttribute("aria-disabled");
    button.removeAttribute("aria-busy");
  }
});

Mantieni l'etichetta come verbo ("Salvataggio", "Invio") invece di sostituirla con un semplice spinner, mantieni fissa la larghezza del pulsante (un min-width evita che si restringa quando il testo cambia) e annuncia il risultato tramite la regione di stato, non solo cambiando il testo del pulsante. aria-busy è un'indicazione che l'elemento si sta aggiornando; gli screen reader si comportano in modo diverso con questo attributo, ed è per questo che l'annuncio vero e proprio lo porta il messaggio di stato.

disabled vs aria-disabled: la vera differenza

Entrambi fanno annunciare il pulsante come non disponibile dalle tecnologie assistive (spesso "attenuato" o "non disponibile"). La differenza sta nel comportamento. Come spiega MDN, l'attributo HTML disabled sopprime ogni funzionalità, mentre aria-disabled="true" espone solo lo stato disabilitato, quindi il comportamento devi sopprimerlo tu.

disabledaria-disabled="true"
Raggiungibile con TabNo, esce dall'ordine di tabulazioneSì, resta focalizzabile
Attivazione con clic e tastieraBloccata dal browserAvviene comunque; il tuo script deve ignorarla
Invia un moduloNo. Il suo nome e il suo valore non vengono inviati, e se è il pulsante predefinito del modulo, premere Invio in un campo non invia il moduloSì, a meno che tu non chiami preventDefault()
Può mostrare un tooltip o una spiegazione al focusNo, non può ricevere il focusSì, con aria-describedby
Aggancio per lo stile:disabled / disabled:[aria-disabled="true"] / aria-disabled:
Funziona sui link (<a>)No, disabled non è valido sui linkSì, insieme alla rimozione di href

Regola pratica: usa disabled quando il pulsante davvero non può essere usato e nessuno ha bisogno di scoprire perché, per esempio un controllo di un modulo dentro una sezione disattivata. Usa aria-disabled quando l'utente deve trovare il pulsante e capire cosa manca ("Inserisci la tua email per continuare"), e durante il caricamento. Spesso la risposta migliore è nessuno dei due: lascia il pulsante attivo e mostra un messaggio di errore chiaro quando viene premuto con dati mancanti.

button.addEventListener("click", (event) => {
  if (button.getAttribute("aria-disabled") === "true") {
    event.preventDefault();   // also stops a submit button from submitting
    return;
  }
  doTheThing();
});

Sul contrasto: il criterio WCAG 1.4.3 esenta dalla regola del contrasto minimo il testo nei componenti inattivi (disabilitati), ed è per questo che un pulsante sbiadito al 50% di opacità è ammesso. Vale comunque la pena mantenere l'etichetta leggibile, perché le persone devono vedere cosa non possono ancora fare.

Pulsanti outline e ghost

Un pulsante outline ha un riempimento trasparente e un bordo visibile; un pulsante ghost non ha nessuno dei due finché non ci passi sopra. Usali per azioni meno importanti accanto a un pulsante primario pieno: outline per una seconda scelta chiara ("Anteprima"), ghost per azioni discrete in barre degli strumenti e menu ("Annulla", "Modifica").

Questi sono pulsanti veri: passaci sopra e premi Tab per vedere ogni stato. Il CSS delle varianti, costruito sopra la base .btn vista prima:

/* Outline: transparent fill, visible border */
.btn-outline {
  background: transparent;
  color: #1d4ed8;
  border-color: #2563eb;
}
.btn-outline:hover:not(:disabled, [aria-disabled="true"]) { background: #eff6ff; }
.btn-outline:active:not(:disabled, [aria-disabled="true"]) { background: #dbeafe; }

/* Ghost: no fill, no visible border until hover */
.btn-ghost {
  background: transparent;
  color: #1d4ed8;
  border-color: transparent;
}
.btn-ghost:hover:not(:disabled, [aria-disabled="true"]) { background: #eff6ff; }
.btn-ghost:active:not(:disabled, [aria-disabled="true"]) { background: #dbeafe; }

/* Destructive */
.btn-danger { background: #dc2626; }
.btn-danger:hover:not(:disabled, [aria-disabled="true"]) { background: #b91c1c; }
.btn-danger:focus-visible { outline-color: #dc2626; }
<!-- Outline -->
<button type="button" class="inline-flex items-center gap-2 rounded-lg border border-blue-600 bg-transparent px-4 py-2.5 text-sm font-semibold text-blue-700 hover:bg-blue-50 active:bg-blue-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-50">
  Outline
</button>

<!-- Ghost -->
<button type="button" class="inline-flex items-center gap-2 rounded-lg border border-transparent px-4 py-2.5 text-sm font-semibold text-blue-700 hover:bg-blue-50 active:bg-blue-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-50">
  Ghost
</button>

<!-- Destructive -->
<button type="button" class="inline-flex items-center gap-2 rounded-lg border border-transparent bg-red-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-red-700 active:bg-red-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:opacity-50">
  Delete project
</button>

Il criterio WCAG 1.4.11 Non-text Contrast chiede un contrasto di 3:1 rispetto ai colori adiacenti per le parti di un controllo necessarie a riconoscerlo. Il bordo blue-600 dell'outline su bianco lo supera. Un pulsante ghost a riposo non ha un contorno, quindi si affida all'etichetta e alla posizione per sembrare cliccabile: tieni i pulsanti ghost dove le persone si aspettano delle azioni, come barre degli strumenti, finestre di dialogo e footer delle card, e mantieni il colore del testo abbastanza scuro per il contrasto del testo normale (4.5:1).

Pulsante di chiusura (X)

Un'icona X non ha testo, quindi ha bisogno di un nome accessibile. Metti aria-label="Close" sul pulsante e aria-hidden="true" sull'SVG, così gli screen reader annunciano "Chiudi, pulsante" invece di niente o del nome di un file. Se una pagina ha più pulsanti di chiusura, rendi i nomi specifici: "Chiudi avviso cookie", "Chiudi finestra".

Le tue modifiche sono state salvate.

<button type="button" class="btn-close" aria-label="Close">
  <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false">
    <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
  </svg>
</button>
.btn-close {
  display: inline-grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;          /* 40px target; WCAG 2.2 AA minimum is 24 by 24 */
  padding: 0;
  border: 0;
  border-radius: 9999px;
  background: transparent;
  color: #334155;
  cursor: pointer;
}
.btn-close:hover { background: rgb(15 23 42 / 0.08); }
.btn-close:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
<button type="button" aria-label="Close"
  class="inline-grid size-10 place-items-center rounded-full text-slate-700 hover:bg-slate-900/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
  <svg viewBox="0 0 24 24" class="size-5" aria-hidden="true" focusable="false">
    <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
  </svg>
</button>

Dimensiona l'area cliccabile, non solo l'icona. Il criterio WCAG 2.5.8 Target Size (Minimum), livello AA in WCAG 2.2, chiede target di almeno 24 per 24 pixel CSS (con eccezioni, come uno spazio sufficiente attorno a un target più piccolo). Il pulsante da 40px qui sopra lo supera con margine mentre la X disegnata resta di 20px. Evita di usare la lettera "x" o il carattere "×" come unico contenuto senza un'etichetta: gli screen reader lo leggono come "x" o "per".

Pulsanti con icona e testo e pulsanti solo icona

<!-- Icon + text: the text is the name, the icon is decoration -->
<button type="button" class="btn">
  <svg aria-hidden="true" focusable="false" width="16" height="16" viewBox="0 0 24 24">...</svg>
  Download PDF
</button>

<!-- Icon only, option 1: aria-label -->
<button type="button" class="btn btn-icon" aria-label="Download PDF">
  <svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 24 24">...</svg>
</button>

<!-- Icon only, option 2: visually hidden text (gets translated by browser translation tools) -->
<button type="button" class="btn btn-icon">
  <svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 24 24">...</svg>
  <span class="visually-hidden">Download PDF</span>
</button>
.btn svg { flex: none; }          /* keep icons from squashing */
.btn-icon {
  width: 2.5rem;
  padding: 0;                       /* square: same width and height */
}
<button type="button" class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-blue-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
  <svg class="size-4 shrink-0" aria-hidden="true" ...></svg>
  Download PDF
</button>

<button type="button" aria-label="Download PDF" class="inline-grid size-10 place-items-center rounded-lg bg-blue-600 text-white hover:bg-blue-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
  <svg class="size-5" aria-hidden="true" ...></svg>
</button>

Checklist prima di pubblicare un pulsante

Altri stili di pulsanti

Per progettare visivamente un pulsante e copiarne il CSS, usa il generatore di pulsanti CSS. Per set già pronti, sfoglia i componenti pulsante e i pulsanti animati. Due risorse che si abbinano bene a questa guida: il set di pulsanti HTML e CSS con stili primary, secondary, outline, ghost, icona, loading e distruttivo, e la raccolta di stili di pulsanti CSS con HTML e CSS da copiare. Per capire dove mettere i pulsanti in una pagina e cosa dovrebbero dire, leggi gli esempi di call to action.

Domande frequenti

Come si applica uno stile a un pulsante disabilitato in CSS?

Usa la pseudo-classe :disabled, per esempio .btn:disabled { opacity: 0.5; cursor: not-allowed; }. Se usi anche aria-disabled, aggiungi [aria-disabled="true"] alla stessa regola. Proteggi gli stili hover e active con :not(:disabled) così un pulsante disabilitato non cambia colore quando ci passi sopra o lo premi.

Qual è la differenza tra disabled e aria-disabled?

disabled toglie il pulsante dall'ordine di tabulazione, blocca i clic e gli impedisce di inviare un modulo. aria-disabled="true" dice solo alle tecnologie assistive che il pulsante non è disponibile: resta focalizzabile e cliccabile, quindi il tuo script deve ignorarne l'attivazione. Usa aria-disabled quando gli utenti devono dare il focus al pulsante per capire perché non è disponibile, o mentre una richiesta è in caricamento.

Per i pulsanti devo usare :focus o :focus-visible?

Usa :focus-visible per l'anello visibile. Il browser lo mostra per il focus da tastiera ma di solito non dopo un clic del mouse, così chi usa la tastiera vede sempre dove si trova senza che compaia un anello a ogni clic. Non rimuovere mai l'outline senza aggiungere un sostituto visibile, perché WCAG 2.4.7 richiede un indicatore di focus visibile.

Come creo un pulsante di chiusura (X) accessibile?

Usa un vero elemento button con type="button" e aria-label="Close" (o un nome più specifico come "Chiudi finestra"), e metti aria-hidden="true" sull'icona X. Rendi l'area cliccabile di almeno 24 per 24 pixel CSS, il minimo WCAG 2.2 AA; da 40 a 44 pixel è più facile da colpire.

Qual è la differenza tra un pulsante outline e un pulsante ghost?

Un pulsante outline ha uno sfondo trasparente e un bordo visibile, quindi si riconosce come pulsante anche a riposo. Un pulsante ghost non ha né sfondo né bordo visibile finché non ci passi sopra, quindi si affida al testo e alla posizione. Usa outline per un'azione secondaria chiara e ghost per azioni discrete in barre degli strumenti o menu.

Come mostro uno stato di caricamento su un pulsante?

Aggiungi uno spinner con aria-hidden="true", cambia l'etichetta in un verbo come Salvataggio, imposta aria-disabled="true" così un secondo clic viene ignorato mentre il focus resta sul pulsante, e annuncia il risultato tramite una regione role="status". Rimuovi gli attributi quando la richiesta termina o fallisce.

Salvataggio delle modifiche in corso

Salvato

Forza uno stato

dal vivo (provalo)

Salvataggio

Salva modifiche

Passaci sopra, raggiungilo con Tab, premilo o clicca per vedere il caricamento.

Stato {state} mostrato.

L'evento click è partito, lo script lo ha ignorato. Inserisci la tua email per continuare.

Clicca entrambi, poi scorrili con Tab: solo uno è raggiungibile.

Continua

Eventi click ricevuti: {n}

Inizia gratis · Nessuna carta di credito richiesta

Correlati: Guide

Prodotto

Per chi è

Funzionalità

Risorse

Azienda

Mappa del sito