AI pisze CSS jak w 2015 roku – i jak to zmienić
Rozwijane menu w nagłówku chowa się pod sekcją hero. Zaznaczasz plik z komponentem, piszesz do asystenta „menu chowa się pod hero, napraw” i po kilku sekundach dostajesz:
css
.menu {position: absolute;z-index: 9999 !important;}
Nie działa. Piszesz „dalej się chowa” – i dostajesz z-index: 99999, a w kolejnej rundzie position: fixed z wyliczonym w JavaScripcie top. W tym samym tygodniu prosisz o poprawę karty produktu, która łamie się w wąskim sidebarze, i model dopisuje trzecie @media (max-width: 768px) z nadpisaniem szerokości. Akordeon w FAQ? max-height: 1000px. Pełnoekranowe hero? height: 100vh. Poziomy scroll na telefonie? body { overflow-x: hidden }.
Każda z tych odpowiedzi jest „poprawna” w tym sensie, że ktoś kiedyś tak pisał i to działało. Ale żadna nie rozwiązuje przyczyny, a część z nich – jak za chwilę zobaczysz – psuje coś innego w miejscu, którego model w ogóle nie oglądał.
Dlaczego model sięga po stare wzorce
Model językowy nie wybiera najlepszego rozwiązania. Wybiera najbardziej prawdopodobne – takie, które najczęściej pojawiało się w podobnym kontekście w danych, na których się uczył. A w przypadku CSS te dane mają wyraźny przechył w przeszłość. Odpowiedzi na Stack Overflow, tutoriale, kod z repozytoriów, artykuły z lat 2010–2020 – to ogromna masa kodu pisanego, zanim pojawiły się cascade layers, container queries, :has() czy jednostki svh. Na jedną dobrą odpowiedź z @layer przypadają setki starszych odpowiedzi z !important.
Do tego dochodzi czas. Funkcje, które dziś są stabilne we wszystkich przeglądarkach, często weszły do nich w ostatnich dwóch, trzech latach. Model może je znać, ale ma o nich dużo mniej przykładów, a jeszcze mniej przykładów poprawnego użycia w prawdziwym projekcie. Zna składnię, ale nie zna nawyku.
Trzeci powód jest najważniejszy, bo nie zniknie wraz z nowszymi modelami: model nie widzi strony. Dostaje plik z komponentem menu, a problem z warstwami prawie nigdy nie leży w menu. Leży w przodku, który tworzy nowy kontekst stosu – przez position: sticky, transform czy opacity – i ten przodek jest w innym pliku, w innym komponencie, czasem w CSS z biblioteki. Model, który widzi tylko .menu, ma do dyspozycji jedną dźwignię: liczbę w z-index. Więc ją podnosi. Robi dokładnie to, co zrobiłby człowiek bez devtools.
Wzorce, które warto rozpoznawać od razu
Kilka odpowiedzi wraca tak często, że warto traktować je jako sygnał „model naprawił objaw, nie przyczynę”:
!importantalboz-indexz kilkoma dziewiątkami. Prawie zawsze oznacza konflikt specyficzności albo kontekst stosu gdzieś wyżej w drzewie. Specyficzność rozwiązują@layer– pisałem o tym w artykule o cascade layers – a warstwy w osi Z rozwiązuje znalezienie przodka tworzącego kontekst.- Media query dla komponentu. Karta, która ma się zmieniać w zależności od miejsca, a nie szerokości ekranu, potrzebuje container queries – tłumaczyłem to w artykule o container queries na przykładzie właśnie takiej karty w sidebarze.
max-height: 1000pxw akordeonie i JavaScript, który czeka natransitionend, zanim ukryje element. Dziś da się animowaćheight: autoidisplay: nonew samym CSS.height: 100vhna sekcji pełnoekranowej. Na telefonie jest wyższe niż widoczny ekran, bovhliczy się przy schowanych paskach przeglądarki.overflow-x: hiddennabody. Ukrywa poziomy scroll, ale po cichu zamieniaoverflow-ynaautoi wyłącza każdyposition: stickyna stronie.- IntersectionObserver, listener
scrollalbo biblioteka do efektów, które CSS umie już sam: animacji powiązanych ze scrollem (opisanych w artykule o scroll-driven animations), wykrywania przyklejonego nagłówka, układu masonry. - Cztery kopie palety kolorów dla dark mode – w
:root, w@media (prefers-color-scheme)i w dwóch selektorach[data-theme].
Każdy z tych punktów rozpiszę w osobnym wpisie w najbliższych tygodniach. Tu ważniejsze jest co innego: wszystkie mają wspólny mianownik. Model wybiera rozwiązanie, które mieści się w pliku, który mu pokazałeś, i które było najczęstsze kilka lat temu.
Plik z zasadami: daj modelowi kontekst, którego nie ma
Pierwszy sposób na zmianę tych nawyków jest prosty i daje zaskakująco dużo: plik z zasadami projektu, który asystent wczytuje przy każdym zadaniu. W Claude Code to CLAUDE.md, w Cursorze pliki w .cursor/rules, w GitHub Copilot .github/copilot-instructions.md, a coraz więcej narzędzi czyta też wspólny AGENTS.md. Nazwa jest różna, mechanizm ten sam: tekst, który trafia do kontekstu modelu, zanim zobaczy twoje polecenie.
Działa to dlatego, że zmienia rozkład prawdopodobieństwa. Bez zasad najbardziej prawdopodobną odpowiedzią na „menu chowa się pod hero” jest z-index: 9999. Z zasadą „nie podnoś z-index, dopóki nie wskażesz przodka tworzącego kontekst stosu” – staje się nią szukanie przodka. Model nie musi „wiedzieć więcej”; wystarczy, że wie, czego oczekujesz.
Tak wygląda sekcja o CSS, od której warto zacząć:
markdown
## CSSCel: przeglądarki z ostatnich 2 lat (Baseline). Nie dodawaj fallbackówdla starszych bez pytania.Architektura- Kolejność warstw: @layer reset, base, components, utilities.Każda reguła należy do jakiejś warstwy.- Zakaz !important poza warstwą utilities.- Style komponentu ograniczaj przez @scope, nie przez długie selektory.Layout- Komponenty reagują na kontener (container queries), nie na viewport.Media query tylko dla layoutu całej strony.- Bez sztywnych height dla elementów z treścią. min-height, grid, subgrid.- Wysokość ekranu: svh (domyślnie) albo dvh. Nigdy 100vh. Nigdy 100vwdo pełnej szerokości.- Przycinanie treści: overflow: clip, nie hidden. Nigdy overflow-x: hiddenna body.Warstwy- z-index tylko ze zmiennych --z-*. Przed zmianą z-index wskaż przodka,który tworzy kontekst stosu, i wyjaśnij, dlaczego.- Korzeń komponentu z warstwami w środku: isolation: isolate.- Modale, menu i tooltipy: <dialog> albo popover (top layer), nie z-index.Kolory i motyw- Tylko tokeny z tokens.css. Nie wprowadzaj nowych wartości hex.- Dark mode: light-dark() + color-scheme. Bez duplikowania palety.Animacje- Animuj transform i opacity. Wejście/wyjście: @starting-style,transition-behavior: allow-discrete. Zawsze prefers-reduced-motion.Zanim naprawisz błąd layoutu, warstw albo przewijania: podaj przyczynę(który element, która właściwość), dopiero potem poprawkę.
Kilka rzeczy w tym pliku jest celowych. Zasady są konkretne – „nigdy overflow-x: hidden na body”, a nie „pisz nowoczesny CSS”, bo ogólne hasło model zinterpretuje po swojemu. Zakazy mają zamienniki – sam zakaz !important sprawi, że model podbije specyficzność selektora; zakaz plus @layer daje mu właściwe narzędzie. Ostatni akapit wymusza kolejność myślenia: najpierw przyczyna, potem kod. To zdanie zmienia odpowiedzi bardziej niż wszystkie pozostałe, bo zmusza model do powiedzenia, czego nie wie – i wtedy widać, że pracuje na zgadywaniu.
Dopasuj plik do projektu. Jeśli wspierasz starsze przeglądarki, napisz które. Jeśli używasz Tailwinda, zasady o warstwach wyglądają inaczej. Plik ma opisywać twój projekt, a nie idealny.
Pułapka: model naprawia objaw, bo nie widzi strony
Plik z zasadami zmienia nawyki, ale nie rozwiązuje trzeciego problemu z początku: model nadal widzi kod, a nie stronę. Zasada „wskaż przodka tworzącego kontekst stosu” niewiele da, jeśli tego przodka nie ma w żadnym pliku, który model przeczytał. Wtedy dostajesz uprzejme zgadywanie: „prawdopodobnie któryś z rodziców ma transform”.
Brakujący element to to, co ty widzisz w devtools: obliczone style i drzewo przodków. Najprostszy sposób, żeby je przekazać, to krótki skrypt w konsoli i wklejenie wyniku do rozmowy:
javascript
// Zaznacz problematyczny element w zakładce Elements (w konsoli: $0)let el = $0const out = []while ((el = el.parentElement)) {const s = getComputedStyle(el)out.push({el: el.tagName.toLowerCase() + (el.className ? "." + el.className.split(" ").join(".") : ""),position: s.position,zIndex: s.zIndex,overflow: s.overflowX + " / " + s.overflowY,transform: s.transform !== "none",opacity: s.opacity,})}copy(JSON.stringify(out, null, 2)) // wynik ląduje w schowku
Wklejasz to razem z poleceniem i nagle model ma te same dane, które miałby doświadczony frontendowiec: widzi, że trzy poziomy wyżej jest header z position: sticky bez z-index, i proponuje z-index na nagłówku zamiast na menu. Odpowiedź przestaje być zgadywaniem.
Drugi, wygodniejszy sposób to danie asystentowi bezpośredniego dostępu do przeglądarki. Agenci w rodzaju Claude Code mogą sterować przeglądarką przez rozszerzenie albo serwer MCP – Playwright, Chrome DevTools – i sami odczytać obliczone style, przejść po drzewie DOM, zrobić zrzut ekranu przed poprawką i po niej. To zmienia charakter pracy: zamiast „napisz poprawkę” możesz poprosić „znajdź, dlaczego menu chowa się pod hero, pokaż mi przyczynę, potem popraw i zrób zrzut ekranu”. Model, który może sprawdzić wynik, przestaje oddawać kod, który „powinien działać”.
Pytaj o przyczynę, nie o kod
Najprostsza zmiana nie wymaga żadnego pliku ani narzędzia – tylko innego polecenia. Porównaj:
markdown
Źle:Menu chowa się pod hero. Napraw.Dobrze:Menu (.site-nav__dropdown) chowa się pod sekcją .hero.Zanim cokolwiek zmienisz: który element tworzy kontekst stosu,w którym jest menu, i dlaczego hero go przykrywa?Nie podnoś z-index samego menu.Poniżej obliczone style przodków menu:[wklejony JSON]
Pierwsze polecenie zostawia modelowi tylko jedną drogę – najkrótszą, czyli najbardziej prawdopodobną. Drugie zamyka tę drogę wprost, daje dane i prosi o wyjaśnienie. Dobra praktyka z code review działa tu tak samo jak przy kodzie od młodszego programisty: jeśli autor poprawki nie umie powiedzieć, dlaczego działa, prawdopodobnie nie działa z powodu, który myśli – albo działa przypadkiem i przestanie przy następnej zmianie.
Druga pułapka: nowa składnia, której nie ma
Gdy już przekonasz model do nowoczesnego CSS, pojawia się odwrotny problem. Funkcje, które dopiero wchodzą do przeglądarek, zmieniają składnię w trakcie standaryzacji – i model potrafi z pełnym przekonaniem napisać wersję, która była w szkicu specyfikacji dwa lata temu i nigdy nie trafiła do żadnej przeglądarki. Albo poprawną składnię funkcji, która działa dziś tylko w jednym silniku, bez słowa o fallbacku.
Taki kod nie zgłasza błędów: nieznana właściwość CSS jest po cichu ignorowana. Dlatego przy każdej funkcji, której nie znasz z własnego doświadczenia, sprawdź ją na MDN albo caniuse, zanim trafi do repozytorium – i dopisz do pliku z zasadami, które nowe funkcje są w projekcie dozwolone, a które wymagają @supports.
Pułapki i dobre praktyki
!importantiz-index: 9999od AI to sygnał, nie rozwiązanie. Prawie zawsze oznaczają konflikt specyficzności albo kontekst stosu w miejscu, którego model nie widział.- Plik z zasadami zmienia nawyki modelu.
CLAUDE.md,.cursor/rules,copilot-instructions.md– konkretne zasady z zamiennikami działają, ogólne hasła nie. - Wymuś kolejność: przyczyna, potem kod. Jedno zdanie w zasadach albo w poleceniu zmienia więcej niż lista zakazów.
- Daj modelowi to, co widzisz w devtools. Obliczone style przodków wklejone do rozmowy albo dostęp do przeglądarki przez MCP zamieniają zgadywanie w diagnozę.
- Sprawdzaj nowe funkcje w źródle. Model może znać nieaktualną składnię albo pominąć brak wsparcia. MDN i caniuse przed commitem.
- Przeglądaj kod od AI jak kod od nowej osoby w zespole. Jeśli poprawka nie ma wyjaśnienia, poproś o nie, zanim ją przyjmiesz.
Podsumowanie
Asystent AI pisze CSS sprzed dekady z trzech powodów: uczył się głównie na starszym kodzie, nowe funkcje zna z dużo mniejszej liczby przykładów, a przede wszystkim nie widzi strony – tylko plik, który mu pokazałeś, więc naprawia objaw jedyną dźwignią, jaką ma pod ręką. Plik z zasadami przestawia jego nawyki na nowoczesny CSS, polecenie „najpierw przyczyna” zmusza do diagnozy zamiast zgadywania, a obliczone style albo dostęp do przeglądarki dają mu to, czego najbardziej brakuje: obraz prawdziwej strony. Żadne z tych narzędzi nie zwalnia z rozumienia CSS – przeciwnie, im lepiej wiesz, dlaczego z-index: 9999 nie pomaga, tym szybciej rozpoznasz, kiedy model próbuje ci go sprzedać. W kolejnych wpisach rozbiorę po kolei problemy, które wymieniłem wyżej.