Dlaczego coraz częściej piszę CSS tam, gdzie kiedyś był JavaScript
W poprzedniej części pisałem o odruchu sięgania po jQuery w nowym projekcie. Ten wpis jest o odruchu subtelniejszym i przez to trwalszym: pisaniu w JavaScripcie rzeczy, które są już w CSS.
Ten nawyk jest głębiej zakorzeniony niż przywiązanie do jednej biblioteki, bo wynika z prawdziwego podziału ról, którego uczyliśmy się latami: CSS odpowiada za wygląd, JavaScript za zachowanie. Cokolwiek się dzieje – reaguje na kliknięcie, zmienia stan, pokazuje i chowa – to była robota dla skryptu. CSS „tylko malował”.
Ten podział przestał być prawdą, a my wciąż piszemy tak, jakby obowiązywał. Bo raz się go nauczyliśmy i tak piszemy dalej – dokładnie ten sam mechanizm, o którym jest cała ta seria. Poniżej konkretnie: co dawniej wymagało skryptu, a dziś jest jedną–dwiema regułami w arkuszu.
Stan zależny od zawartości: :has()
Klasyczny wzorzec: „jeśli w środku jest X, ostyluj rodzica”. CSS nie umiał patrzeć w górę drzewa, więc dorzucaliśmy klasę skryptem – i to był jeden z najczęstszych powodów, dla których w ogóle pisało się JavaScript do stylowania:
// Kiedyś: skrypt tylko po to, żeby ostylować rodzica
document.querySelectorAll('.pole').forEach((pole) => {
if (pole.querySelector(':invalid')) pole.classList.add('ma-blad');
});:has() to pełnoprawny selektor rodzica (Baseline od grudnia 2023). Ten sam efekt bez ani jednej linijki skryptu:
/* Pole formularza zawierające błędne wejście */
.pole:has(:invalid) { border-color: crimson; }
/* Przycisk wysyłki wygaszony, gdy formularz ma błędy */
form:has(:invalid) button[type="submit"] { opacity: .5; pointer-events: none; }Co ważne, i o czym łatwo zapomnieć: :has() reaguje na :invalid, :checked czy :empty na żywo, bez podpinania listenerów. Stan interfejsu opisuje arkusz, nie skrypt.
Rozwijane sekcje: <details>
Akordeon to był rytuał: listener na nagłówku, przełączanie klasy, animacja wysokości. Tymczasem HTML ma to natywnie od dawna:
<details>
<summary>Pokaż szczegóły</summary>
<p>Treść rozwija i zwija sama przeglądarka. Zero JavaScriptu.</p>
</details>Działa z klawiatury, jest dostępne dla czytników ekranu, pamięta stan otwarcia. Grupę <details> można nawet spiąć atrybutem name, żeby zachowywały się jak klasyczny akordeon „tylko jedna otwarta naraz” – też bez skryptu.
Dymki, menu i tooltipy: Popover API
Pokazanie i schowanie dymka to było zarządzanie widocznością, warstwami z-index i obsługą „kliknij obok, żeby zamknąć”. Popover API (Baseline od stycznia 2025) załatwia to dwoma atrybutami:
<button popovertarget="menu">Menu</button>
<div id="menu" popover>
<a href="/blog">Blog</a>
<a href="/portfolio">Portfolio</a>
</div>Zamknięcie kliknięciem obok, warstwa wierzchnia (top-layer, więc dymek nie chowa się pod innymi elementami) i obsługa Escape są wbudowane. To jest dokładnie ta klasa mikro-skryptów, którą pisało się w każdym projekcie od nowa.
Modal: <dialog>
Okno modalne to był overlay, blokada przewijania tła, pułapka focusa (żeby Tab nie uciekł za modal) i zamykanie Escape. Element <dialog> daje to wszystko z pakietu:
dialog.showModal(); // pułapka focusa i przyciemnione tło (::backdrop) za darmoFormularz z method="dialog" zamyka okno i przekazuje wynik przez dialog.returnValue – nawet logika „OK / Anuluj” nie potrzebuje osobnego listenera.
Pozycjonowanie względem elementu: anchor positioning
„Przyklej dymek pod przyciskiem i odbij się od krawędzi okna” to był sztandarowy powód, dla którego ludzie dołączali Popper.js albo Floating UI. CSS umie to natywnie:
.przycisk { anchor-name: --menu-btn; }
.dymek {
position: absolute;
position-anchor: --menu-btn;
top: anchor(bottom); /* pod dolną krawędzią kotwicy */
justify-self: anchor-center;
}To funkcja świeża – na sierpień 2026 wspierają ją Chrome i Safari 26, Firefox jest w drodze (cel Interop 2026). Dlatego traktuj ją jako progresywne ulepszenie, przez @supports, z Floating UI jako fallbackiem tam, gdzie pozycja jest krytyczna.
Drobiazgi, które odruchowo pisaliśmy skryptem
Te są Baseline od dawna, a każdy z osobna kasował kiedyś kawałek JavaScriptu:
.panel {
position: sticky; /* sticky nagłówek – koniec biblioteki „sticky” i liczenia scrollTop */
top: 0;
}
html { scroll-behavior: smooth; } /* płynne przewijanie do kotwic bez skryptu */
.galeria {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory; /* karuzela „zatrzymaj się na slajdzie” bez JS */
}
.galeria > * { scroll-snap-align: start; }
.tresc { columns: 2; } /* układ wielokolumnowy bez liczenia wysokości */Pasek postępu czytania artykułu – kiedyś addEventListener('scroll', …) z przeliczaniem pozycji – to dziś animacja podpięta pod oś przewijania:
@keyframes rosnij { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.pasek-postepu {
transform-origin: left;
animation: rosnij auto linear;
animation-timeline: scroll(root block); /* świeże – owinąć w @supports */
}Dlaczego to nie jest tylko „mniej kodu”
Można na to spojrzeć jak na oszczędność linijek, ale prawdziwy zysk jest gdzie indziej. Kiedy stan interfejsu opisuje CSS, dostajesz trzy rzeczy za darmo:
- Dostępność w pakiecie.
<dialog>,<details>i Popover mają wbudowaną obsługę klawiatury, focusa i czytników ekranu. Odtworzenie tego w JavaScripcie jest trudne i zwykle wychodziło niekompletnie. - Zero kosztu wykonania. Reguła CSS nie czeka na pobranie, sparsowanie i wykonanie skryptu. Nie ma migotania między „HTML gotowy” a „JS podpiął zachowanie”.
- Działa, zanim (i jeśli) JavaScript wystartuje. Akordeon na
<details>rozwija się nawet przy zablokowanym albo jeszcze niezaładowanym skrypcie. To odporność, nie tylko elegancja.
Sekcja uczciwości: granice
CSS przejął zaskakująco dużo, ale nie wszystko – i udawanie, że przejął, byłoby tym samym błędem, tylko w drugą stronę:
- Świeże funkcje z nierównym wsparciem. Anchor positioning i scroll-driven animations na sierpień 2026 nie są jeszcze pełnym Baseline (zwykle brakuje jednego silnika). Używaj ich przez
@supports, nie usuwaj fallbacku, dopóki MDN nie pokaże „widely available”. - Złożona logika to wciąż JavaScript. Walidacja zależna od danych z serwera, dynamiczne listy, cokolwiek z realnym stanem aplikacji – to zostaje w skrypcie. CSS opisuje stan, nie liczy go.
- Natywne ≠ zwolnienie z myślenia.
<dialog>daje dobrą bazę dostępności, ale wciąż trzeba zadbać o etykiety, sensowną kolejność focusa iprefers-reduced-motion. Fundament jest mocny, ale to nadal fundament, nie cały dom.
Puenta
Podział „CSS maluje, JavaScript działa” był prawdziwy przez większość historii weba i dlatego wrył się głęboko. Ale przeglądarki po cichu przesunęły granicę: stan, interakcja, a nawet reakcja na przewijanie w wielu przypadkach zeszły do arkusza stylów. Odruch „to zachowanie, więc piszę skrypt” wart jest dziś jednej sekundy zastanowienia – bo coraz częściej to zachowanie potrafi opisać CSS, taniej i pewniej.
W ostatniej części serii pójdę o poziom wyżej: od pojedynczych bibliotek i pojedynczych reguł do samego frameworka. Zapytam, kiedy sięganie po React czy Vue jest decyzją, a kiedy – jak jQuery w części pierwszej – tylko odruchem.