Stylowanie rodzica na podstawie stanu dziecka to coś, czego webdeveloperzy chcieli od lat – zwłaszcza przy budowaniu formularzy, nawigacji i komponentów interaktywnych z zachowaniem wysokiej dostępności. Dzięki pseudoklasom :focus-within i :has() CSS wreszcie to potrafi – i to bez JavaScriptu.
Problem – „chcę ostylować rodzica, gdy dziecko ma focus”
Przykład, który zna każdy: mamy kontener .field z etykietą i polem <input>. Chcemy, aby całe pole (ramka, tło) podświetlało się, gdy focus ma input w środku – tak jak w nowoczesnych formularzach.
Przez lata CSS działał tylko „w dół” drzewa DOM: mogliśmy wybrać dzieci na podstawie rodzica, ale nie odwrotnie. Aby zmienić styl rodzica na podstawie dziecka (np. input:focus), trzeba było używać:
- JavaScriptu / jQuery (
focus/blur+classList.add/remove()), - sztuczek z dodatkowym elementem (np. pseudo‑elementy, z-index),
- albo projektować HTML inaczej niż by się chciało.
Specyfikacja Selectors Level 4 wprowadziła wreszcie funkcjonalną pseudoklasę :has(), która pozwala wybrać element, jeśli ma zawarte w sobie elementy spełniające dany selektor – czyli w praktyce pozwala stylować rodzica na podstawie dzieci. Zanim jednak :has() zaczęło być szeroko wspierane, pojawiła się pseudoklasa :focus-within, rozwiązująca przynajmniej ten jeden, bardzo częsty przypadek – focusu.
Podstawy: jak działa :focus-within?
Pseudoklasa :focus-within wybiera element, jeśli on sam ma focus lub którykolwiek z jego potomków ma focus.
Innymi słowy: to odpowiednik „ma focus gdzieś w środku”.
Przykład z MDN‑owskiej definicji da się streścić tak:
:focus-withinreprezentuje element, który sam pasuje do:focus, albo ma potomka, który pasuje do:focus.
Prosty przykład z formularzem
HTML:
<div class="field">
<label for="email">E-mail</label>
<input id="email" type="email">
</div>
CSS:
.field {
border: 1px solid #999;
padding: 0.75rem;
border-radius: 0.5rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.field:focus-within {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25);
}
Gdy użytkownik przeniesie focus na <input>, focus nie jest na .field, ale .field:focus-within będzie pasował, bo wewnątrz niego znajduje się element z :focus.
To dokładnie ten scenariusz, którego brak przez lata wymuszał stosowanie JavaScriptu.
Dostępność – dlaczego to ważne
Z perspektywy dostępności :focus-within jest szczególnie cenny, bo:
- pozwala wzmocnić widoczność focusu – np. podświetlając całe pole, nie tylko samą ramkę inputa,
- ułatwia orientację przestrzenną osobom korzystającym z klawiatury: „który blok formularza mam aktualnie?”,
- pozwala stworzyć większe „hity” wizualne bez manipulowania strukturą DOM.
Ważne: nie zastępuj :focus-visible, tylko raczej go uzupełniaj. Poniżej przykład, w którym fokus jest wyraźny głównie przy nawigacji klawiaturą:
.field input:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
.field:focus-within {
background: #eff6ff;
}
Pseudoklasa :has() – brakujący selektor rodzica (i dużo więcej)
:has() to funkcjonalna pseudoklasa CSS, która pozwala wybrać element, jeśli którykolwiek z przekazanych selektorów pasuje do co najmniej jednego elementu znajdującego się w jego zakresie. Może sprawdzać potomków, rodzeństwo, a nawet elementy później w drzewie względem elementu, który wybieramy.
Definicja z bloga Chrome DevTools:
:has()reprezentuje element, jeśli którykolwiek z selektorów przekazanych jako parametry pasuje do co najmniej jednego elementu.
Składnia
Podstawowa postać wygląda tak:
element:has(selektor-relatywny) {
/* style */
}
Przykłady użycia w praktyce:
/* Ostyluj .field, jeśli ma w środku input w focusie */
.field:has(input:focus) {}
/* Ostyluj <li>, jeśli zawiera link z klasą .active */
li:has(a.active) {}
/* Ostyluj .card, jeśli zawiera obrazek */
.card:has(img) {}
selektor-relatywny w środku :has() może być złożony, korzystać z pseudoklas (:focus, :checked, :invalid) i łączyć różne warunki.
Wsparcie w przeglądarkach
Dziś sytuacja jest zupełnie inna niż na starcie specyfikacji:
- Chromium (Chrome, Edge) wspiera
:has()od wersji 105, - Safari wdrożyło
:has()wcześnie (15.4+), - Firefox dołączył później, ale obecnie także posiada implementację (aktualny stan warto zawsze sprawdzić w narzędziach typu „Can I use”).
Globalne wsparcie przekracza już 90% użytkowników, co oznacza, że :has() można realnie używać w produkcji – z rozsądnymi fallbackami.
Stylowanie rodzica z :has() – praktyczne wzorce
Formularz – rodzic reagujący na focus inputa
Ten sam formularz co wcześniej, ale z użyciem :has():
<div class="field">
<label for="email">E-mail</label>
<input id="email" type="email" required>
</div>
.field {
border: 1px solid #999;
padding: 0.75rem;
border-radius: 0.5rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
/* rodzic, który ma w sobie input z focus */
.field:has(input:focus) {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25);
}
To jest bardziej ogólne niż :focus-within, bo możemy bardzo precyzyjnie określić, który element wewnątrz musi mieć focus (np. tylko <input> albo tylko <textarea>).
Nawigacja – aktywny element w menu przy focusie linku
Przykład inspirowany wzorcem z artykułów o :has():
<ul class="nav">
<li><a href="/">Start</a></li>
<li><a href="/oferta">Oferta</a></li>
<li><a href="/kontakt">Kontakt</a></li>
</ul>
.nav li {
padding: 0.5rem 1rem;
border-radius: 9999px;
transition: background-color 0.2s;
}
.nav li:has(a:focus) {
background-color: #e5e7eb;
}
W tym układzie focus ma link <a>, natomiast ostylowany zostaje rodzic <li>, który odpowiada za całą „pigułkę” nawigacyjną – czytelniejsze i wygodniejsze wizualnie.
Karty interaktywne – hover, focus i focus-within naraz
Możemy też wykorzystać :has() do łączenia kilku stanów:
.card {
border-radius: 0.75rem;
padding: 1.5rem;
background: #fff;
transition: transform 0.15s, box-shadow 0.15s;
}
/* ostyluj kartę, gdy:
- jest hover na niej samej
- lub któryś element w środku ma focus */
.card:hover,
.card:has(:focus-visible),
.card:focus-within {
box-shadow: 0 15px 40px rgba(15, 23, 42, 0.18);
transform: translateY(-2px);
}
Z punktu widzenia dostępności:
- użytkownicy myszy widzą efekt na
:hover, - użytkownicy klawiatury dostają ten sam efekt dzięki
:focus-visible/:focus-within, :has()daje możliwość scalenia tego w elegancki warunek „cokolwiek w środku jest w focusie” bez przebudowy markup.
Pseudoklasy :focus-within vs :has(:focus) – podobieństwa i różnice
Koncepcyjnie
:focus-within odpowiada mniej więcej „ma gdzieś w środku focus”, ale działa tylko na focus i ma z góry zdefiniowaną logikę. Z kolei :has() jest ogólne: możemy napisać :has(:focus), :has(input:focus), :has(a.active), :has(:checked) i wiele innych kombinacji.
Tabela porównawcza
Poniżej zestawienie kluczowych różnic między podejściami:
| Cecha | :focus-within |
:has(:focus) |
|---|---|---|
| Zakres | tylko focus | dowolne selektory (w tym :focus) |
| Wyrażalność | „ma focus gdzieś w środku” | dowolna zależność od potomków / rodzeństwa |
| Czytelność | bardzo wysoka przy przypadkach z focusem | wymaga znajomości :has(), ale nadal dość czytelne |
| Wsparcie przeglądarek | bardzo dobre, od lat w głównych przeglądarkach | dobre w nowoczesnych (Chrome 105+, Safari 15.4+ itd.) |
| Wydajność | relatywnie tanie | potencjalnie droższe, lepiej używać rozważnie |
| Zastosowania | formularze, menu, komponenty fokusowalne | rozbudowane logiki UI, walidacja, przełączniki, layouty |
Kiedy którego użyć?
Jeśli chodzi tylko o focus – zacznij od :focus-within. Jest świetnie wspierany, prosty i wystarczający do ogromnej liczby przypadków.
Jeśli potrzebujesz większej precyzji lub łączenia warunków (np. „tylko input[type="text"] z focus” lub połączenia ze stanami :hover, :checked, :invalid) – sięgnij po :has() jako progressive enhancement.
Przykład hybrydowy – najpierw :focus-within, potem ulepszamy :has():
.field:focus-within {
border-color: #2563eb;
}
/* Nowoczesne przeglądarki: focus tylko na inputach tekstowych */
@supports selector(.field:has(input[type="text"]:focus)) {
.field {
border-color: #999; /* reset */
}
.field:has(input[type="text"]:focus) {
border-color: #2563eb;
}
}
Pseudoklasa :has() poza focusem – potężne wzorce dla dostępności
Skoro mamy już selektor rodzica, warto wykorzystać go szerzej niż tylko dla focusu.
Walidacja formularzy – rodzic z błędem
HTML:
<div class="field">
<label for="email">E-mail</label>
<input id="email" type="email" required>
<p class="error" aria-live="polite">Podaj poprawny adres e-mail.</p>
</div>
CSS:
.field .error {
display: none;
}
/* rodzic, który zawiera nieprawidłowe pole */
.field:has(input:invalid) {
border-color: #b91c1c;
background: #fef2f2;
}
.field:has(input:invalid) .error {
display: block;
}
Zalety stosowania tego podejścia:
- bez JavaScriptu reagujesz na natywny stan walidacji (
:invalid,:valid), - komunikat błędu jest powiązany strukturalnie z polem,
- możesz dodatkowo podpiąć komunikat pod
aria-describedbydla czytników ekranu.
Przełączniki i menu rozwijane
Typowy pattern:
<button class="menu-toggle" aria-expanded="false"> Menu </button>
<nav class="menu">
...
</nav>
Możesz zmieniać styl kontenera w zależności od stanu wewnętrznego przełącznika:
.header {
transition: box-shadow 0.2s;
}
/* gdy w .header jest przycisk z aria-expanded="true" */
.header:has(.menu-toggle[aria-expanded="true"]) {
box-shadow: 0 4px 20px rgba(15, 23, 42, 0.2);
}
Taki pattern łączy semantyczne atrybuty ARIA z czytelną logiką stylów.
Strona reagująca na stan komponentów
Możesz nawet stylować body na podstawie tego, co dzieje się głębiej w UI:
body:has(.dialog[open]) {
overflow: hidden;
}
Dzięki temu, gdy natywny <dialog> jest otwarty ([open]), blokujesz scroll w tle – bez dodatkowych klas i JS (poza samym otwieraniem dialogu).
Progressive enhancement i fallbacki
Dyrektywa @supports selector(…)
Implementacje wspierają @supports selector(...), więc możesz warunkowo użyć :has() tylko tam, gdzie jest dostępne:
/* Podstawowe style dostępne wszędzie */
.field {
border: 1px solid #999;
}
/* Ulepszenia z :has() tylko tam, gdzie jest wspierane */
@supports selector(.field:has(input:focus)) {
.field:has(input:focus) {
border-color: #2563eb;
}
}
W starszych przeglądarkach użytkownik nadal otrzyma działający formularz, tylko bez dodatkowego efektu.
Łączenie z :focus-within jako fallback
Dobry, praktyczny pattern łączący szerokie wsparcie i precyzję warunków:
/* Działa prawie wszędzie: focus-within */
.field:focus-within {
border-color: #2563eb;
}
/* W nowoczesnych: precyzyjniejsze warunki z :has() */
@supports selector(.field:has(input:focus)) {
.field {
border-color: #999;
}
.field:has(input:focus) {
border-color: #2563eb;
}
}
Wydajność i dobre praktyki
:has() jest potężne, ale to nie jest najtańszy selektor. Przy masowym stosowaniu w bardzo złożonym DOM-ie może wpłynąć na wydajność. Warto kierować się tymi zasadami:
- zawężaj selektor przed
:has()– np..field:has(input:focus)zamiast*:has(input:focus), - unikaj
:has()na bardzo ogólnych elementach (jakbody:has(...)) w połączeniu z szerokimi selektorami w środku, jeśli nie ma takiej potrzeby, - używaj go do logiki UI, która naprawdę wymaga patrzenia „w dół”, a nie jako zamiennik dla prostych selektorów potomków.
:focus-within jest pod tym względem „tańsze” – ma węższy zakres i prostszą semantykę.
Wzorce komponentów przyjaznych dostępności z :focus-within i :has()
Dostępne pola formularza
HTML bazowy:
<div class="field">
<label for="name">Imię i nazwisko</label>
<input id="name" name="name" autocomplete="name" required>
<p class="hint" id="name-hint">Np. Jan Kowalski</p>
</div>
Powiązanie z ARIA:
<input id="name" aria-describedby="name-hint">
CSS:
.field {
border: 1px solid #d4d4d8;
border-radius: 0.75rem;
padding: 0.75rem 1rem;
background: #fafafa;
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
}
/* wzmocnienie focusu dla całego bloku */
.field:focus-within {
border-color: #2563eb;
background: #eff6ff;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25);
}
/* dodatkowo: błąd walidacji */
.field:has(input:invalid) {
border-color: #b91c1c;
background: #fef2f2;
}
Efekt pracy powyższych stylów:
- wyraźny fokus dla klawiatury,
- czytelne oznaczenie błędów,
- zachowanie spójne w całym formularzu.
Lista z klikalnymi kafelkami
HTML:
<ul class="cards">
<li class="card">
<a href="#">
<h2>Plan podstawowy</h2>
<p>Idealny na start.</p>
</a>
</li>
<li class="card">
<a href="#">
<h2>Plan Pro</h2>
<p>Dla wymagających.</p>
</a>
</li>
</ul>
CSS:
.card {
padding: 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 0 0 0 #111827;
transition: box-shadow 0.3s ease-out, transform 0.3s ease-out;
}
.card a {
display: block;
color: inherit;
text-decoration: none;
outline: none;
}
/* gdy link w nagłówku ma focus – podświetl kartę */
.card:has(h2 > a:focus-visible) {
box-shadow: 0 0 0 4px #111827;
transform: translateY(-2px);
}
Z punktu widzenia dostępności fokus jest bardzo wyraźny i obejmuje całą kartę, a osoba korzystająca z klawiatury dostaje takie same „pigułki” jak użytkownik myszy.
Podsumowanie praktyczne (w formie checklisty)
Bez formalnego podsumowania, ale z listą, którą możesz zastosować od razu:
- używaj
:focus-withinzawsze, gdy chcesz, by rodzic reagował na focus wewnątrz, - sięgaj po
:has(), gdy potrzebujesz bardziej złożonych warunków – focus, walidacja, przełączniki, stan komponentów, - projektuj z myślą o dostępności,
- nie usuwaj całkowicie fokusów (np.
outline: none) bez zapewnienia równoważnego zamiennika, - wzmacniaj sygnały wizualne (kontrast, obrys, tło),
- łącz
:focus-visible,:focus-withini:has(:focus)świadomie, - stosuj progressive enhancement z
@supports selector(...), aby:has()nie psuł layoutu w starszych przeglądarkach, - dbaj o wydajność – zawężaj selektory przed
:has()i nie nadużywaj go tam, gdzie wystarczy prostszy selektor.






