Stany przycisków CSS: hover, focus, active, disabled, przyciski outline i z ikoną

Stany przycisków CSS to style, które przycisk pokazuje po najechaniu (:hover), przy fokusie z klawiatury (:focus-visible), podczas wciśnięcia (:active), po wyłączeniu (:disabled lub aria-disabled) i w trakcie ładowania. Oto jeden przycisk w każdym stanie, z czystym CSS i Tailwind do skopiowania, a także dostępne przyciski outline, ghost, zamykania i z ikoną.

Przycisk ma sześć stanów, które warto zaprojektować: domyślny, hover, focus-visible, active, disabled i ładowanie. Większość fragmentów kodu w sieci pokazuje jeden lub dwa z nich, na różnych przyciskach i z różnym kodem. Ten poradnik przez cały czas używa jednego przycisku, pokazuje wszystkie stany obok siebie i dla każdego podaje czysty CSS oraz Tailwind do skopiowania. Następnie omawia najczęściej wyszukiwane warianty (outline, ghost, zamykania, z ikoną) oraz różnicę między disabled a aria-disabled, która jest czymś więcej niż kwestią stylu.

Macierz stanów: jeden przycisk, sześć stanów

Każdy kafelek poniżej pokazuje jeden stan, dzięki czemu porównasz je jednym rzutem oka. Działający przycisk pod siatką pozwala wymusić stan albo wypróbować go własną myszą i klawiaturą.

Zapisz zmiany
Domyślny. Stan spoczynku. Biały na blue-600 (#2563eb) daje kontrast około 5.2:1.
Zapisz zmiany
Hover. Kursor nad przyciskiem. O jeden stopień ciemniej, bez zmiany układu.
Zapisz zmiany
Focus-visible. Osiągnięty klawiaturą. Pierścień 2px z odstępem 2px, widoczny na każdym tle.
Zapisz zmiany
Active. Podczas wciśnięcia. Jeszcze ciemniej i przesunięty w dół o 1px.
Zapisz zmiany
Disabled. Niedostępny. Przygaszony, bez hover, bez kursora wskazującego.
Zapisywanie
Ładowanie. Trwa praca. Spinner plus czasownik mówiący, co się dzieje.
StanSelektor CSSWariant TailwindCo go wywołuje
Domyślny.btn(bez prefiksu)Nic
Hover.btn:hoverhover:Kursor nad przyciskiem. Ekrany dotykowe nie mają prawdziwego hover.
Focus-visible.btn:focus-visiblefocus-visible:Fokus, który przeglądarka uznaje za wart pokazania, zwykle fokus z klawiatury (Tab)
Focus.btn:focusfocus:Każdy fokus, w większości przeglądarek także po kliknięciu myszą
Active.btn:activeactive:Gdy przycisk myszy, palec lub klawisz Space jest przytrzymany
Disabled.btn:disableddisabled:Atrybut disabled
Disabled (ARIA).btn[aria-disabled="true"]aria-disabled:Atrybut aria-disabled="true"
Ładowanie.btn[aria-busy="true"]aria-busy:Twój skrypt, w trakcie trwania pracy

W czystym CSS kolejność ma znaczenie, gdy selektory mają równą specyficzność: najpierw :hover, potem :focus-visible, potem :active, aby wciśnięcie wygrywało z najechaniem. Tailwind sortuje swoje warianty za Ciebie.

Pełny przycisk w czystym 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; }
}

Większość pracy wykonują trzy szczegóły. Zabezpieczenie :not(:disabled, [aria-disabled="true"]) sprawia, że wyłączony przycisk nie reaguje na hover ani wciśnięcie, przez co inaczej wyglądałby na klikalny. Przezroczyste obramowanie utrzymuje pełny przycisk w tym samym rozmiarze co wariant outline, więc zamiana wariantów nie przesuwa układu. A :focus-visible zamiast :focus oznacza, że użytkownicy myszy nie widzą pierścienia po kliknięciu, a użytkownicy klawiatury widzą go zawsze.

Używaj go tak: <button type="button" class="btn">Save changes</button>. Zawsze ustawiaj type: <button> wewnątrz formularza domyślnie ma type="submit" i po kliknięciu wyśle formularz.

Ten sam przycisk w 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>

Te klasy działają w Tailwind 3.4 i 4. Dwie uwagi dotyczące v4: hover: działa tylko na urządzeniach obsługujących hover (@media (hover: hover)), a style bazowe v4 nadają przyciskom cursor: default, więc dodaj cursor-pointer, jeśli chcesz kursor w kształcie dłoni. Pozostałe zmienione ustawienia domyślne, takie jak ring o grubości 1px, znajdziesz w ściądze Tailwind v4.

Focus-visible: stan, którego nie można pominąć

WCAG 2.4.7 Focus Visible (poziom AA) wymaga, aby fokus klawiatury był widoczny. Usunięcie obrysu przeglądarki przez outline: none bez dodania niczego w zamian nie spełnia tego kryterium. Wskazówki, które się sprawdzają:

Stan ładowania

Przycisk w stanie ładowania musi robić cztery rzeczy: pokazywać, że trwa praca, blokować drugie wysłanie, zachować fokus klawiatury tam, gdzie był, i informować użytkowników czytników ekranu o zakończeniu pracy. Ustawienie disabled na czas żądania łamie trzeci punkt: przycisk traci fokus, a w niektórych przeglądarkach użytkownicy klawiatury wracają na początek strony. Zamiast tego użyj aria-disabled i zabezpieczenia w skrypcie.

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

Zostaw etykietę w formie czasownika ("Zapisywanie", "Wysyłanie") zamiast zastępować ją samym spinnerem, utrzymuj stałą szerokość przycisku (min-width nie pozwala mu się kurczyć przy zmianie tekstu) i ogłaszaj wynik przez region statusu, a nie wyłącznie zmianą tekstu przycisku. aria-busy to wskazówka, że element jest aktualizowany. Czytniki ekranu różnie ją traktują, dlatego właściwy komunikat niesie wiadomość statusu.

disabled a aria-disabled: prawdziwa różnica

Oba sprawiają, że technologie wspomagające ogłaszają przycisk jako niedostępny (często "wygaszony" lub "niedostępny"). Różnica dotyczy zachowania. Jak ujmuje to MDN, atrybut HTML disabled wyłącza całą funkcjonalność, a aria-disabled="true" tylko udostępnia informację o stanie wyłączenia, więc zachowanie musisz zablokować samodzielnie.

disabledaria-disabled="true"
Dostępny klawiszem TabNie, wypada z kolejności tabulacjiTak, nadal może otrzymać fokus
Aktywacja kliknięciem i klawiaturąBlokowana przez przeglądarkęNadal następuje: Twój skrypt musi ją ignorować
Wysyła formularzNie. Jego nazwa i wartość nie są wysyłane, a jeśli jest domyślnym przyciskiem formularza, naciśnięcie Enter w polu nie wysyła formularzaTak, chyba że wywołasz preventDefault()
Może pokazać podpowiedź lub wyjaśnienie przy fokusieNie, nie może otrzymać fokusuTak, z aria-describedby
Punkt zaczepienia dla stylów:disabled / disabled:[aria-disabled="true"] / aria-disabled:
Działa na linkach (<a>)Nie, disabled nie jest poprawny na linkachTak, w połączeniu z usunięciem href

Praktyczna zasada: używaj disabled, gdy przycisku naprawdę nie da się użyć i nikt nie musi odkrywać dlaczego, na przykład kontrolki formularza w wyłączonej sekcji. Używaj aria-disabled, gdy użytkownik musi znaleźć przycisk i dowiedzieć się, czego brakuje ("Podaj swój e-mail, aby kontynuować"), oraz podczas ładowania. Często najlepsza odpowiedź to żadne z nich: zostaw przycisk włączony i pokaż jasny komunikat o błędzie, gdy zostanie naciśnięty przy brakujących danych.

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

O kontraście: WCAG 1.4.3 zwalnia tekst w nieaktywnych (wyłączonych) komponentach z wymogu minimalnego kontrastu, dlatego przygaszony przycisk z przezroczystością 50% jest dozwolony. Mimo to warto zachować czytelność etykiety, bo ludzie muszą widzieć, czego jeszcze nie mogą zrobić.

Przyciski outline a ghost

Przycisk outline ma przezroczyste wypełnienie i widoczne obramowanie, a przycisk ghost nie ma ani jednego, ani drugiego, dopóki nie najedziesz na niego kursorem. Używaj ich do mniej ważnych akcji obok pełnego przycisku głównego: outline dla wyraźnego drugiego wyboru ("Podgląd"), ghost dla cichych akcji w paskach narzędzi i menu ("Anuluj", "Edytuj").

To prawdziwe przyciski: najedź na nie kursorem i naciśnij Tab, aby zobaczyć każdy stan. CSS dla wariantów, nałożony na powyższą bazę .btn:

/* 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 wymaga kontrastu 3:1 względem sąsiednich kolorów dla części kontrolki potrzebnych do jej rozpoznania. Obramowanie blue-600 wariantu outline na białym tle to spełnia. Przycisk ghost w spoczynku nie ma granicy, więc to etykieta i położenie sprawiają, że wygląda na klikalny: umieszczaj przyciski ghost tam, gdzie ludzie spodziewają się akcji, na przykład w paskach narzędzi, oknach dialogowych i stopkach kart, a kolor ich tekstu utrzymuj na tyle ciemny, by spełniał kontrast zwykłego tekstu (4.5:1).

Przycisk zamykania (X)

Ikona X nie ma tekstu, więc potrzebuje dostępnej nazwy. Dodaj aria-label="Close" do przycisku i aria-hidden="true" do SVG, aby czytniki ekranu ogłaszały "Zamknij, przycisk" zamiast niczego albo nazwy pliku. Jeśli strona ma kilka przycisków zamykania, nadaj im konkretne nazwy: "Zamknij informację o cookies", "Zamknij okno dialogowe".

Twoje zmiany zostały zapisane.

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

Wymiaruj obszar kliknięcia, a nie tylko ikonę. WCAG 2.5.8 Target Size (Minimum), poziom AA w WCAG 2.2, wymaga celów o wielkości co najmniej 24 na 24 piksele CSS (z wyjątkami, na przykład gdy wokół mniejszego celu jest wystarczająco dużo miejsca). Powyższy przycisk 40px spełnia to z zapasem, a narysowany X ma nadal 20px. Unikaj używania litery "x" lub znaku "×" jako jedynej zawartości bez etykiety: czytniki ekranu odczytują go jako "x" lub "razy".

Przyciski z ikoną i tekstem oraz same ikony

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

Lista kontrolna przed wdrożeniem przycisku

Więcej stylów przycisków

Aby zaprojektować przycisk wizualnie i skopiować jego CSS, użyj generatora przycisków CSS. Gotowe zestawy znajdziesz wśród komponentów przycisków i animowanych przycisków. Dwa elementy, które dobrze uzupełniają ten poradnik: zestaw przycisków HTML i CSS ze stylami primary, secondary, outline, ghost, z ikoną, ładowaniem i akcją destrukcyjną oraz kolekcja stylów przycisków CSS z HTML i CSS do skopiowania. O tym, gdzie umieszczać przyciski na stronie i co powinny mówić, przeczytasz w przykładach wezwań do działania.

Najczęściej zadawane pytania

Jak ostylować wyłączony przycisk w CSS?

Użyj pseudoklasy :disabled, na przykład .btn:disabled { opacity: 0.5; cursor: not-allowed; }. Jeśli używasz też aria-disabled, dodaj [aria-disabled="true"] do tej samej reguły. Zabezpiecz style hover i active przez :not(:disabled), aby wyłączony przycisk nie zmieniał koloru po najechaniu lub wciśnięciu.

Czym różni się disabled od aria-disabled?

disabled usuwa przycisk z kolejności tabulacji, blokuje kliknięcia i nie pozwala mu wysłać formularza. aria-disabled="true" tylko informuje technologie wspomagające, że przycisk jest niedostępny: nadal można na niego przenieść fokus i go kliknąć, więc Twój skrypt musi ignorować aktywację. Używaj aria-disabled, gdy użytkownicy muszą przenieść fokus na przycisk, aby dowiedzieć się, dlaczego jest niedostępny, albo podczas ładowania żądania.

Używać :focus czy :focus-visible dla przycisków?

Do widocznego pierścienia używaj :focus-visible. Przeglądarka pokazuje go przy fokusie z klawiatury, ale zwykle nie po kliknięciu myszą, więc użytkownicy klawiatury zawsze widzą, gdzie są, a pierścień nie pojawia się przy każdym kliknięciu. Nigdy nie usuwaj obrysu bez dodania widocznego zamiennika, bo WCAG 2.4.7 wymaga widocznego wskaźnika fokusu.

Jak zrobić dostępny przycisk zamykania (X)?

Użyj prawdziwego elementu button z type="button" i aria-label="Close" (albo bardziej konkretną nazwą, na przykład "Close dialog") i dodaj aria-hidden="true" do ikony X. Obszar kliknięcia powinien mieć co najmniej 24 na 24 piksele CSS, czyli minimum WCAG 2.2 AA. W przycisk o wielkości od 40 do 44 pikseli łatwiej trafić.

Czym różni się przycisk outline od przycisku ghost?

Przycisk outline ma przezroczyste tło i widoczne obramowanie, więc w spoczynku wygląda jak przycisk. Przycisk ghost nie ma tła ani widocznego obramowania, dopóki nie najedziesz na niego kursorem, więc polega na tekście i położeniu. Używaj outline dla wyraźnej akcji drugorzędnej, a ghost dla cichych akcji w paskach narzędzi lub menu.

Jak pokazać stan ładowania na przycisku?

Dodaj spinner z aria-hidden="true", zmień etykietę na czasownik, na przykład Zapisywanie, ustaw aria-disabled="true", aby drugie kliknięcie było ignorowane, a fokus pozostał na przycisku, i ogłoś wynik przez region role="status". Usuń atrybuty, gdy żądanie się zakończy lub nie powiedzie.

Zapisywanie zmian

Zapisano

Wymuś stan

na żywo (wypróbuj)

Zapisywanie

Zapisz zmiany

Najedź kursorem, przejdź do niego klawiszem Tab, wciśnij go lub kliknij, aby zobaczyć ładowanie.

Pokazano stan {state}.

Zdarzenie click zostało wywołane, skrypt je zignorował. Podaj swój e-mail, aby kontynuować.

Kliknij oba, a potem przejdź po nich klawiszem Tab: tylko do jednego da się dotrzeć.

Kontynuuj

Odebrane zdarzenia click: {n}

Zacznij za darmo · Bez karty kredytowej

Powiązane: Poradniki

Produkt

Dla kogo

Funkcje

Zasoby

Firma

Mapa strony