CSS-Button-Zustände: Hover, Focus, Active, Disabled, Outline- und Icon-Buttons

CSS-Button-Zustände sind die Styles, die ein Button zeigt, wenn mit der Maus darüber gefahren wird (:hover), er per Tastatur fokussiert (:focus-visible), gedrückt (:active), deaktiviert (:disabled oder aria-disabled) wird oder lädt. Hier ist ein Button in jedem Zustand, mit kopierbarem reinem CSS und Tailwind, plus Outline-, Ghost-, Schließen- und Icon-Buttons, die barrierefrei bleiben.

Ein Button hat sechs Zustände, die gestaltet werden sollten: Standard, Hover, Focus-visible, Active, Disabled und Loading. Die meisten Snippets im Netz zeigen nur ein oder zwei davon, an unterschiedlichen Buttons, mit unterschiedlichem Code. Dieser Guide nutzt durchgehend einen einzigen Button, zeigt alle Zustände nebeneinander und liefert für jeden kopierbares reines CSS und Tailwind. Danach geht es um die Varianten, nach denen am häufigsten gesucht wird (Outline, Ghost, Schließen, Icon), und um den Unterschied zwischen disabled und aria-disabled, der mehr ist als eine Stilfrage.

Die Zustandsmatrix: ein Button, sechs Zustände

Jede Kachel unten zeigt einen Zustand, damit du sie auf einen Blick vergleichen kannst. Mit dem Live-Button unter dem Raster kannst du einen Zustand erzwingen oder ihn mit deiner eigenen Maus und Tastatur ausprobieren.

Änderungen speichern
Standard. Ruhezustand. Weiß auf blue-600 (#2563eb) ergibt einen Kontrast von etwa 5.2:1.
Änderungen speichern
Hover. Mauszeiger darüber. Eine Stufe dunkler; keine Layoutänderung.
Änderungen speichern
Focus-visible. Per Tastatur erreicht. Ein 2px-Ring mit 2px Abstand, damit er auf jedem Hintergrund erkennbar ist.
Änderungen speichern
Active. Während er gedrückt wird. Noch einmal dunkler und 1px nach unten versetzt.
Änderungen speichern
Disabled. Nicht verfügbar. Verblasst, kein Hover, kein Zeiger.
Wird gespeichert
Loading. Arbeitet. Spinner plus ein Verb, das sagt, was gerade passiert.
ZustandCSS-SelektorTailwind-VarianteWas ihn auslöst
Standard.btn(kein Präfix)Nichts
Hover.btn:hoverhover:Ein Zeiger über dem Button. Touchscreens kennen kein echtes Hover.
Focus-visible.btn:focus-visiblefocus-visible:Fokus, den der Browser anzeigen will, typischerweise Tastaturfokus (Tab)
Focus.btn:focusfocus:Jeder Fokus, in den meisten Browsern auch ein Mausklick
Active.btn:activeactive:Solange Maustaste, Finger oder Leertaste gedrückt gehalten wird
Disabled.btn:disableddisabled:Das Attribut disabled
Disabled (ARIA).btn[aria-disabled="true"]aria-disabled:Das Attribut aria-disabled="true"
Loading.btn[aria-busy="true"]aria-busy:Dein Skript, solange die Arbeit läuft

In reinem CSS kommt es auf die Reihenfolge an, wenn die Selektoren dieselbe Spezifität haben: zuerst :hover, dann :focus-visible, dann :active, damit Drücken über Hovern gewinnt. Tailwind sortiert seine Varianten für dich.

Der komplette Button in reinem CSS

.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; }
}

Drei Details erledigen den Großteil der Arbeit. Die Absicherung :not(:disabled, [aria-disabled="true"]) verhindert, dass ein deaktivierter Button auf Hover oder Drücken reagiert und dadurch klickbar wirkt. Der transparente Rahmen hält einen gefüllten Button genauso groß wie einen Outline-Button, sodass sich das Layout beim Wechsel der Variante nicht verschiebt. Und :focus-visible statt :focus bedeutet, dass Mausnutzer nach dem Klicken keinen Ring sehen, Tastaturnutzer aber immer.

So verwendest du ihn: <button type="button" class="btn">Save changes</button>. Setze immer type: Ein <button> in einem Formular hat standardmäßig type="submit" und sendet das Formular beim Klicken ab.

Derselbe Button 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>

Diese Klassen funktionieren in Tailwind 3.4 und 4. Zwei Hinweise zu v4: hover: greift nur auf Geräten, die Hover unterstützen (@media (hover: hover)), und die Basis-Styles von v4 geben Buttons cursor: default. Füge also cursor-pointer hinzu, wenn du den Hand-Cursor willst. Die anderen geänderten Standardwerte, etwa den 1px-ring, findest du im Tailwind-v4-Spickzettel.

Focus-visible: der Zustand, den du nicht weglassen darfst

WCAG 2.4.7 Focus Visible (Stufe AA) verlangt, dass der Tastaturfokus sichtbar ist. Wer den Browser-Rahmen mit outline: none entfernt und nichts ergänzt, fällt durch. Richtlinien, die sich bewähren:

Der Loading-Zustand

Ein Button im Ladezustand muss vier Dinge leisten: zeigen, dass gearbeitet wird, ein zweites Absenden verhindern, den Tastaturfokus an seiner Stelle halten und Screenreader-Nutzern mitteilen, wann die Arbeit fertig ist. Wer während der Anfrage disabled setzt, verletzt den dritten Punkt: Der Button verliert den Fokus, und Tastaturnutzer landen in manchen Browsern wieder am Seitenanfang. Nutze stattdessen aria-disabled und sichere im Skript ab.

<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");
  }
});

Lass die Beschriftung ein Verb sein ("Wird gespeichert", "Wird gesendet"), statt sie nur durch einen Spinner zu ersetzen, halte die Breite des Buttons fest (ein min-width verhindert, dass er schrumpft, wenn sich der Text ändert) und verkünde das Ergebnis über die Status-Region, nicht nur durch eine Änderung des Button-Texts. aria-busy ist ein Hinweis, dass das Element gerade aktualisiert wird; Screenreader gehen unterschiedlich damit um, deshalb übernimmt die Statusmeldung die eigentliche Ansage.

disabled vs aria-disabled: der echte Unterschied

Bei beiden sagen Hilfstechnologien den Button als nicht verfügbar an (oft als "abgeblendet" oder "nicht verfügbar"). Der Unterschied liegt im Verhalten. Wie MDN es formuliert, unterdrückt das HTML-Attribut disabled jede Funktion, während aria-disabled="true" nur den deaktivierten Zustand mitteilt, das Verhalten musst du also selbst unterdrücken.

disabledaria-disabled="true"
Mit Tab erreichbarNein, er verlässt die Tab-ReihenfolgeJa, er bleibt fokussierbar
Aktivierung per Klick und TastaturVom Browser blockiertWird weiterhin ausgelöst; dein Skript muss sie ignorieren
Sendet ein Formular abNein. Name und Wert werden nicht gesendet, und ist er der Standard-Button des Formulars, sendet Enter in einem Feld nicht abJa, außer du rufst preventDefault() auf
Kann beim Fokus einen Tooltip oder eine Erklärung zeigenNein, er kann keinen Fokus erhaltenJa, mit aria-describedby
Ansatzpunkt fürs Styling:disabled / disabled:[aria-disabled="true"] / aria-disabled:
Funktioniert bei Links (<a>)Nein, disabled ist bei Links nicht gültigJa, kombiniert mit dem Entfernen von href

Faustregel: Nutze disabled, wenn der Button wirklich nicht verwendet werden kann und niemand herausfinden muss, warum, zum Beispiel ein Formularelement in einem abgeschalteten Abschnitt. Nutze aria-disabled, wenn Nutzer den Button finden und erfahren müssen, was fehlt ("Gib deine E-Mail-Adresse ein, um fortzufahren"), und während des Ladens. Oft ist keins von beiden die beste Lösung: Lass den Button aktiv und zeig eine klare Fehlermeldung, wenn er mit fehlenden Eingaben gedrückt wird.

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

Zum Kontrast: WCAG 1.4.3 nimmt Text in inaktiven (deaktivierten) Komponenten von der Mindestkontrast-Regel aus, deshalb ist ein auf 50% Deckkraft verblasster Button erlaubt. Trotzdem lohnt es sich, die Beschriftung lesbar zu halten, denn Leute müssen sehen, was sie noch nicht tun können.

Outline- vs Ghost-Buttons

Ein Outline-Button hat eine transparente Füllung und einen sichtbaren Rahmen; ein Ghost-Button hat beides erst beim Hover. Setze sie für weniger wichtige Aktionen neben einen gefüllten Primär-Button ein: Outline für eine klare zweite Wahl ("Vorschau"), Ghost für leise Aktionen in Toolbars und Menüs ("Abbrechen", "Bearbeiten").

Das sind echte Buttons: Fahr mit der Maus darüber und drück Tab, um jeden Zustand zu sehen. CSS für die Varianten, aufbauend auf der .btn-Basis von oben:

/* 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>

WCAG 1.4.11 Non-text Contrast verlangt einen Kontrast von 3:1 zu angrenzenden Farben für die Teile eines Bedienelements, an denen man es erkennt. Der blue-600-Rahmen des Outline-Buttons auf Weiß schafft das. Ein Ghost-Button hat im Ruhezustand keine Begrenzung und ist darauf angewiesen, dass Beschriftung und Platzierung ihn klickbar wirken lassen: Setze Ghost-Buttons dorthin, wo Leute Aktionen erwarten, etwa in Toolbars, Dialoge und Karten-Footer, und halte ihre Textfarbe dunkel genug für normalen Textkontrast (4.5:1).

Schließen-Button (X)

Ein X-Icon hat keinen Text und braucht daher einen zugänglichen Namen. Setze aria-label="Close" auf den Button und aria-hidden="true" auf das SVG, damit Screenreader "Schließen, Button" ansagen statt gar nichts oder einen Dateinamen. Hat eine Seite mehrere Schließen-Buttons, mach die Namen konkret: "Cookie-Hinweis schließen", "Dialog schließen".

Deine Änderungen wurden gespeichert.

<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>

Bemiss die Klickfläche, nicht nur das Icon. WCAG 2.5.8 Target Size (Minimum), in WCAG 2.2 auf Stufe AA, verlangt Ziele von mindestens 24 mal 24 CSS-Pixeln (mit Ausnahmen, etwa genug Abstand um ein kleineres Ziel). Der 40px-Button oben erfüllt das mit viel Luft, während das gezeichnete X 20px groß bleibt. Verwende den Buchstaben "x" oder das Zeichen "×" nicht ohne Label als einzigen Inhalt: Screenreader lesen es als "x" oder "mal" vor.

Buttons mit Icon und Text und reine Icon-Buttons

<!-- 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>

Checkliste, bevor du einen Button auslieferst

Weitere Button-Stile

Um einen Button visuell zu gestalten und sein CSS zu kopieren, nutze den CSS-Button-Generator. Für fertige Sets stöbere in den Button-Komponenten und den animierten Buttons. Zwei Bausteine, die gut zu diesem Guide passen: das HTML- und CSS-Button-Set mit Primär-, Sekundär-, Outline-, Ghost-, Icon-, Loading- und Lösch-Stilen und die Sammlung von CSS-Button-Stilen mit kopierbarem HTML und CSS. Wo Buttons auf einer Seite sitzen und was darauf stehen sollte, liest du in den Call-to-Action-Beispielen.

Häufige Fragen

Wie style ich einen deaktivierten Button in CSS?

Sprich die Pseudoklasse :disabled an, zum Beispiel .btn:disabled { opacity: 0.5; cursor: not-allowed; }. Wenn du auch aria-disabled nutzt, ergänze [aria-disabled="true"] in derselben Regel. Sichere deine Hover- und Active-Styles mit :not(:disabled) ab, damit ein deaktivierter Button beim Hovern oder Drücken nicht die Farbe wechselt.

Was ist der Unterschied zwischen disabled und aria-disabled?

disabled nimmt den Button aus der Tab-Reihenfolge, blockiert Klicks und verhindert, dass er ein Formular absendet. aria-disabled="true" teilt Hilfstechnologien nur mit, dass der Button nicht verfügbar ist: Er bleibt fokussierbar und klickbar, dein Skript muss die Aktivierung also ignorieren. Nutze aria-disabled, wenn Nutzer den Button fokussieren müssen, um zu erfahren, warum er nicht verfügbar ist, oder während eine Anfrage lädt.

Soll ich für Buttons :focus oder :focus-visible verwenden?

Nutze :focus-visible für den sichtbaren Ring. Der Browser zeigt ihn bei Tastaturfokus, meist aber nicht nach einem Mausklick. So sehen Tastaturnutzer immer, wo sie sind, ohne dass bei jedem Klick ein Ring erscheint. Entferne die Outline nie, ohne einen sichtbaren Ersatz hinzuzufügen, denn WCAG 2.4.7 verlangt einen sichtbaren Fokusindikator.

Wie baue ich einen barrierefreien Schließen-Button (X)?

Nutze ein echtes button-Element mit type="button" und aria-label="Close" (oder einem konkreteren Namen wie "Close dialog") und setze aria-hidden="true" auf das X-Icon. Mach die klickbare Fläche mindestens 24 mal 24 CSS-Pixel groß, das Minimum nach WCAG 2.2 AA; 40 bis 44 Pixel sind leichter zu treffen.

Was ist der Unterschied zwischen einem Outline-Button und einem Ghost-Button?

Ein Outline-Button hat einen transparenten Hintergrund und einen sichtbaren Rahmen, daher wirkt er schon im Ruhezustand wie ein Button. Ein Ghost-Button hat weder Hintergrund noch sichtbaren Rahmen, bis man mit der Maus darüber fährt, und ist auf Text und Position angewiesen. Nutze Outline für eine klare sekundäre Aktion und Ghost für leise Aktionen in Toolbars oder Menüs.

Wie zeige ich einen Ladezustand an einem Button?

Füge einen Spinner mit aria-hidden="true" hinzu, ändere die Beschriftung in ein Verb wie Wird gespeichert, setze aria-disabled="true", damit ein zweiter Klick ignoriert wird, während der Fokus auf dem Button bleibt, und verkünde das Ergebnis über eine role="status"-Region. Entferne die Attribute, wenn die Anfrage abgeschlossen oder fehlgeschlagen ist.

Deine Änderungen werden gespeichert

Gespeichert

Zustand erzwingen

live (ausprobieren)

Wird gespeichert

Änderungen speichern

Fahr mit der Maus darüber, spring mit Tab hin, drück ihn oder klick, um den Ladezustand zu sehen.

Zeigt den Zustand {state}.

Klick-Event ausgelöst, das Skript hat es ignoriert. Gib deine E-Mail-Adresse ein, um fortzufahren.

Klick beide an und geh dann mit Tab durch: Nur einer ist erreichbar.

Weiter

Empfangene Klick-Events: {n}

Kostenlos starten · Keine Kreditkarte nötig

Verwandt: Ratgeber

Produkt

Für wen

Funktionen

Ressourcen

Unternehmen

Sitemap