Moderní CSS v roce 2026: Vlastnosti, které změnily způsob, jakým píšu styly
Container queries, CSS layers, :has(), color-mix(), nesting, scroll-driven animace a anchor positioning. CSS vlastnosti, díky kterým jsem přestal sahat po JavaScriptu.
Svůj poslední Sass soubor jsem smazal před šesti měsíci. Ne proto, že bych dělal prohlášení. Protože jsem ho prostě už nepotřeboval.
Více než deset let bylo CSS jazykem, za který jsme se omlouvali. Potřebovali jsme preprocesory pro zanořování a proměnné. Potřebovali jsme JavaScript pro velikost založenou na kontejnerech, animace propojené se scrollem, výběr rodičovských prvků a polovinu layoutových vzorů, které nám designéři předávali. Vybudovali jsme celé runtime systémy — CSS-in-JS knihovny, utilitní frameworky, řetězce PostCSS pluginů — abychom kompenzovali to, co jazyk nativně neuměl.
Ta éra skončila. Ne "skoro skončila." Ne "blíží se." Skončila.
Vlastnosti, které přistály v prohlížečích mezi lety 2024 a 2026, nepřidaly jen pohodlí. Změnily mentální model. CSS už není stylovací jazyk, proti kterému bojujete. Je to stylovací jazyk, který skutečně přemýšlí o komponentách, správě specificity, responzivním designu na úrovni prvků a animacích bez runtime.
Zde je to, co se změnilo, proč to záleží a s čím díky tomu můžete přestat.
Container Queries: Konec otázky "Jak široký je viewport?"#
Toto je ta vlastnost, která zásadně změnila můj přístup k responzivnímu designu. Dvacet let jsme měli media queries. Media queries se ptají: "jak široký je viewport?" Container queries se ptají: "jak široký je kontejner, ve kterém tato komponenta žije?"
Ten rozdíl zní nepatrně. Není. Je to rozdíl mezi komponentami, které fungují pouze v jednom kontextu rozvržení, a komponentami, které fungují všude.
Problém, který existoval dvě desetiletí#
Představte si komponentu kartičky. V postranním panelu by se měla řadit vertikálně s malým obrázkem. V oblasti hlavního obsahu by měla být horizontální s větším obrázkem. V plnošíré hero sekci by měla vypadat úplně jinak.
S media queries byste napsali něco takového:
/* Starý způsob: propojení stylů komponenty s rozložením stránky */
.card {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.sidebar .card {
/* Stále vertikální v postranním panelu */
}
.main-content .card {
flex-direction: row;
/* Horizontální v hlavní oblasti */
}
}
@media (min-width: 1200px) {
.hero .card {
/* Ještě jiné rozložení */
}
}Kartička ví o .sidebar, .main-content a .hero. Ví o stránce. Už to není komponenta — je to fragment vědomý stránky. Přesuňte ji na jinou stránku a všechno se rozbije.
Container Queries to kompletně řeší#
/* Způsob container query: komponenta ví pouze o sobě */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
.card {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@container card (min-width: 400px) {
.card {
grid-template-columns: 200px 1fr;
}
}
@container card (min-width: 700px) {
.card {
grid-template-columns: 300px 1fr;
gap: 2rem;
font-size: 1.125rem;
}
}Kartička neví, kde žije. A je jí to jedno. Vložte ji do 300px postranního panelu a je vertikální. Vložte ji do 700px hlavní oblasti a je horizontální. Umístěte ji do plnošíré sekce a přizpůsobí se. Nulová znalost rozložení stránky vyžadována.
inline-size vs size#
Téměř vždy budete chtít container-type: inline-size. To umožňuje dotazy na inline osu (šířku v horizontálních režimech psaní). Použití container-type: size umožňuje dotazy jak na inline, tak na blokovou osu, ale vyžaduje to, aby kontejner měl explicitní rozměry v obou dimenzích, což ve většině případů narušuje normální tok dokumentu.
/* Tohle chcete v 99 % případů */
.wrapper {
container-type: inline-size;
}
/* Tohle vyžaduje explicitní výšku — zřídka to, co chcete */
.wrapper-both {
container-type: size;
height: 500px; /* vyžadováno, jinak kolabuje */
}Pojmenované kontejnery pro vnořené kontexty#
Když vnořujete kontejnery, pojmenování se stává zásadním:
.page-layout {
container-type: inline-size;
container-name: layout;
}
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
/* Cílit konkrétně na postranní panel, ne na nejbližšího předka */
@container sidebar (max-width: 250px) {
.nav-item {
font-size: 0.875rem;
padding: 0.25rem;
}
}
/* Cílit na rozložení stránky */
@container layout (min-width: 1200px) {
.page-header {
font-size: 2.5rem;
}
}Bez názvů @container dotazuje nejbližšího předka s omezením. Při vnořování to často není kontejner, který chcete. Pojmenujte je. Vždy.
Jednotky Container Query#
Tato vlastnost je podceňovaná. Jednotky container query (cqi, cqb, cqw, cqh) vám umožňují dimenzovat věci relativně ke kontejneru, ne k viewportu:
@container (min-width: 400px) {
.card-title {
font-size: clamp(1rem, 4cqi, 2rem);
}
}4cqi jsou 4 % inline velikosti kontejneru. Nadpis se škáluje s kontejnerem, ne s oknem. Toto je to, čím fluidní typografie měla být od začátku.
CSS Layers: Válka o specificitu skončila#
Pokud container queries změnily můj přístup k responzivnímu designu, @layer změnil můj přístup k CSS architektuře. Poprvé máme rozumný, deklarativní způsob správy specificity napříč celým projektem.
Problém#
CSS specificita je bodový systém, který se nestará o vaše záměry. Utilitní třída s .text-red prohrává proti .card .title, protože ta druhá má vyšší specificitu. Řešení bylo vždy stejné: udělejte své selektory specifičtější, přidejte !important, nebo přestrukturujte všechno.
Vybudovali jsme celé metodologie (BEM, SMACSS, ITCSS) a toolchainy jen proto, abychom se vyhnuli konfliktům specificity. To všechno byl workaround pro chybějící jazykovou vlastnost.
Řazení vrstev#
@layer vám umožňuje deklarovat pořadí, ve kterém jsou skupiny stylů zvažovány, bez ohledu na specificitu uvnitř těchto skupin:
/* Deklarace pořadí vrstev — tento jediný řádek řídí vše */
@layer reset, base, components, utilities, overrides;
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
line-height: 1.6;
color: oklch(20% 0 0);
}
h1, h2, h3 {
line-height: 1.2;
text-wrap: balance;
}
}
@layer components {
.card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px oklch(0% 0 0 / 0.12);
}
.card .title {
font-size: 1.25rem;
font-weight: 600;
color: oklch(25% 0.05 260);
}
}
@layer utilities {
.text-red {
color: oklch(55% 0.25 25);
}
.hidden {
display: none;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
}I když .card .title má vyšší specificitu než .text-red, utilita vyhrává, protože vrstva utilities je deklarovaná za components. Žádné !important. Žádné hacky na specificitu. Pořadí vrstev má poslední slovo.
Jak Tailwind CSS v4 používá vrstvy#
Tailwind v4 se silně opírá o @layer. Když napíšete @import "tailwindcss", dostanete:
@layer theme, base, components, utilities;Každá Tailwind utilita žije ve vrstvě utilities. Vaše vlastní styly komponent jdou do components. Proto třída text-red-500 může přepsat barvu komponenty bez potřeby !important — je v pozdější vrstvě.
Pokud stavíte vlastní design systém bez Tailwindu, ukradněte tuto architekturu. Je správná:
@layer reset, tokens, base, layouts, components, utilities, overrides;Sedm vrstev bohatě stačí. Nikdy jsem nepotřeboval víc.
Nevrstevnaté styly přebijí vše#
Jeden háček: styly, které nejsou v žádné vrstvě, mají nejvyšší prioritu. To je vlastně užitečné — znamená to, že vaše jednorázové overridy specifické pro stránku automaticky vyhrávají:
@layer components {
.modal {
background: white;
}
}
/* Mimo jakoukoli vrstvu — vyhrává nad vším ve vrstvách */
.special-page .modal {
background: oklch(95% 0.02 260);
}Ale to také znamená, že CSS třetích stran, které nepoužívají vrstvy, může přepsat celý váš systém. Obalte styly třetích stran do vrstvy, abyste je měli pod kontrolou:
@layer third-party {
@import url("some-library.css");
}Selektor :has(): Rodičovský selektor, který jsme vždy chtěli#
Doslova po desetiletí vývojáři žádali rodičovský selektor. "Chci stylovat rodiče na základě jeho potomků." Odpověď byla vždy "CSS to neumí," následovaná JavaScriptovým workaroundem. :has() to kompletně mění a ukázalo se, že je ještě mocnější než to, o co jsme žádali.
Základní výběr rodiče#
/* Stylovat skupinu formuláře, když její input má focus */
.form-group:has(input:focus) {
border-color: oklch(55% 0.2 260);
box-shadow: 0 0 0 3px oklch(55% 0.2 260 / 0.15);
}
/* Stylovat kartičku jinak, když obsahuje obrázek */
.card:has(img) {
padding-top: 0;
}
.card:has(img) .card-content {
padding: 1.5rem;
}
/* Kartička bez obrázku dostane jiné zacházení */
.card:not(:has(img)) {
border-left: 4px solid oklch(55% 0.2 260);
}Validační stavy formuláře bez JavaScriptu#
Tady se :has() stává opravdu vzrušujícím. V kombinaci s HTML validačními pseudo-třídami můžete vytvářet formulářová UI, která reagují na stav validity s nulovým JavaScriptem:
/* Obal pole reaguje na validitu svého inputu */
.field:has(input:invalid:not(:placeholder-shown)) {
--field-color: oklch(55% 0.25 25);
}
.field:has(input:valid:not(:placeholder-shown)) {
--field-color: oklch(55% 0.2 145);
}
.field {
--field-color: oklch(70% 0 0);
border: 2px solid var(--field-color);
border-radius: 0.5rem;
padding: 0.75rem;
transition: border-color 0.2s;
}
.field label {
color: var(--field-color);
font-size: 0.875rem;
font-weight: 500;
}
.field .error-message {
display: none;
color: oklch(55% 0.25 25);
font-size: 0.8rem;
margin-top: 0.25rem;
}
.field:has(input:invalid:not(:placeholder-shown)) .error-message {
display: block;
}Část :not(:placeholder-shown) je klíčová — zabraňuje zobrazení validačních stylů na prázdných polích, kterých se uživatel ještě nedotkl.
Rozložení založené na počtu potomků#
Tento vzor je absurdně užitečný a byl skutečně nemožný před :has():
/* Přizpůsobit sloupce gridu podle počtu položek */
.grid-auto:has(> :nth-child(4)) {
grid-template-columns: repeat(2, 1fr);
}
.grid-auto:has(> :nth-child(7)) {
grid-template-columns: repeat(3, 1fr);
}
.grid-auto:has(> :nth-child(13)) {
grid-template-columns: repeat(4, 1fr);
}Grid mění svůj počet sloupců na základě toho, kolik má potomků. Žádný JavaScript. Žádný ResizeObserver. Žádné přepínání tříd.
Detekce postranního panelu#
Jeden z mých oblíbených vzorů — změna rozložení na základě toho, zda existuje postranní panel:
.page-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
/* Pokud rozložení obsahuje postranní panel, přepnout na dva sloupce */
.page-layout:has(.sidebar) {
grid-template-columns: 1fr 300px;
}
/* Přizpůsobit šířku hlavního obsahu, když je přítomen postranní panel */
.page-layout:has(.sidebar) .main-content {
max-width: 65ch;
}Přidejte komponentu postranního panelu do DOM, rozložení se přizpůsobí. Odeberte ji, přizpůsobí se zpět. CSS je zdrojem pravdy, ne stavová proměnná.
Kombinace :has() s dalšími selektory#
:has() se krásně skládá se vším ostatním:
/* Stylovat článek pouze když má specifickou třídu figure */
article:has(figure.full-bleed) {
overflow: visible;
}
/* Navigace, která se změní, když obsahuje vyhledávací input */
nav:has(input[type="search"]:focus) {
background: oklch(98% 0 0);
box-shadow: 0 2px 8px oklch(0% 0 0 / 0.08);
}
/* Zapnout dark-mode na úrovni komponenty */
.theme-switch:has(input:checked) ~ main {
color-scheme: dark;
background: oklch(15% 0 0);
color: oklch(90% 0 0);
}Podpora prohlížečů je nyní vynikající. Každý moderní prohlížeč podporuje :has() od začátku roku 2024. Není důvod ho nepoužívat.
CSS Nesting: Nativní, konečně stabilní#
Nebudu tuto vlastnost přechvalovat. CSS nesting je příjemný. Není revoluční tak jako container queries nebo :has(). Ale odstraňuje jeden z posledních důvodů k používání Sassu, a to je důležité.
Syntaxe#
.card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
.title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.description {
color: oklch(40% 0 0);
line-height: 1.7;
}
&:hover {
box-shadow: 0 4px 12px oklch(0% 0 0 / 0.1);
}
&.featured {
border: 2px solid oklch(60% 0.2 260);
}
}Toto je validní CSS. Žádný build krok. Žádný preprocesor. Žádný PostCSS plugin. Prohlížeč to zpracovává nativně.
Rozdíly oproti Sassu#
Existuje několik syntaktických rozdílů, které stojí za zmínku:
/* CSS Nesting — funguje nyní ve všech prohlížečích */
.parent {
/* Přímé zanořování tříd/prvků funguje bez & */
.child {
color: red;
}
/* & je vyžadováno pro pseudo-třídy a složené selektory */
&:hover {
background: blue;
}
&.active {
font-weight: bold;
}
/* Vnořené media queries — to je skvělé */
@media (width >= 768px) {
flex-direction: row;
}
}V raných implementacích jste potřebovali & před selektory prvků (jako & p místo jen p). Toto omezení bylo uvolněno. Od roku 2025 všechny hlavní prohlížeče podporují holé zanořování prvků: .parent { p { ... } } funguje bez problémů.
Vnořené Media Queries#
Toto je podle mého názoru klíčová vlastnost CSS nestingu. Ne zanořování selektorů — schopnost umístit media queries dovnitř bloku pravidel:
.hero {
padding: 2rem;
font-size: 1rem;
@media (width >= 768px) {
padding: 4rem;
font-size: 1.25rem;
}
@media (width >= 1200px) {
padding: 6rem;
font-size: 1.5rem;
}
}Porovnejte to se starým způsobem, kdy vaše styly .hero byly roztroušeny přes tři různé @media bloky, možná stovky řádků od sebe. Nesting udržuje responzivní chování spolu s komponentou. Čitelnost se dramaticky zlepšuje.
Nepřenořujte příliš#
Jedno varování: jen proto, že můžete zanořovat šest úrovní hluboko, neznamená, že byste měli. Stejná rada ze Sassu platí i zde. Pokud vaše zanořování vytváří selektory jako .page .section .card .content .text .highlight, vytvořili jste monstrum specificity a noční můru údržby. Dvě nebo tři úrovně jsou ideální.
/* Dobře — dvě úrovně */
.nav {
.link {
color: inherit;
&:hover {
color: oklch(55% 0.2 260);
}
}
}
/* Špatně — noční můra specificity */
.page {
.layout {
.sidebar {
.nav {
.list {
.item {
.link {
color: red; /* hodně štěstí při přepisování */
}
}
}
}
}
}
}Barevné funkce: oklch() a color-mix() mění vše#
hsl() mělo dobrý běh. Bylo intuitivnější než rgb(). Ale má zásadní vadu: není perceptuálně uniformní. hsl(60, 100%, 50%) (žlutá) vypadá dramaticky světlejší pro lidské oko než hsl(240, 100%, 50%) (modrá), přestože mají stejnou hodnotu světlosti.
Proč oklch() vyhrává#
oklch() je perceptuálně uniformní. Stejné hodnoty světlosti vypadají stejně světle. To enormně záleží při generování barevných palet, vytváření motivů a zajištění přístupného kontrastu:
:root {
/* oklch(světlost chroma odstín) */
--color-primary: oklch(55% 0.2 260); /* modrá */
--color-secondary: oklch(55% 0.2 330); /* fialová */
--color-success: oklch(55% 0.2 145); /* zelená */
--color-danger: oklch(55% 0.25 25); /* červená */
--color-warning: oklch(75% 0.18 85); /* žlutá — vyšší L pro stejné vnímání */
/* Všechny tyto vypadají stejně "středně" pro lidské oko */
/* S HSL byste potřebovali různé hodnoty světlosti pro každý odstín */
}Tři hodnoty jsou intuitivní, jakmile jim porozumíte:
- Světlost (0 % až 100 %): Jak světlé nebo tmavé. 0 % je černá, 100 % je bílá.
- Chroma (0 až ~0,37): Jak živé. 0 je šedá, vyšší je sytější.
- Odstín (0 až 360): Barevný úhel. 0/360 je růžovočervená, 145 je zelená, 260 je modrá.
color-mix() pro odvozené barvy#
color-mix() vám umožňuje vytvářet barvy z jiných barev za běhu. Žádná Sass funkce darken(). Žádný JavaScript. Jen CSS:
:root {
--brand: oklch(55% 0.2 260);
/* Zesvětlit smícháním s bílou */
--brand-light: color-mix(in oklch, var(--brand) 30%, white);
/* Ztmavit smícháním s černou */
--brand-dark: color-mix(in oklch, var(--brand) 70%, black);
/* Vytvořit jemné pozadí */
--brand-bg: color-mix(in oklch, var(--brand) 8%, white);
/* Poloprůhledná verze */
--brand-overlay: color-mix(in oklch, var(--brand) 50%, transparent);
}
.button {
background: var(--brand);
color: white;
&:hover {
background: var(--brand-dark);
}
&:active {
background: color-mix(in oklch, var(--brand) 60%, black);
}
}
.button.secondary {
background: var(--brand-bg);
color: var(--brand-dark);
&:hover {
background: color-mix(in oklch, var(--brand) 15%, white);
}
}Část in oklch je důležitá. Míchání v srgb dává kalné přechodové barvy. Míchání v oklch dává perceptuálně rovnoměrné výsledky. Vždy míchat v oklch.
Sestavení kompletní palety s oklch()#
Takto generuji celou paletu odstínů z jednoho hue:
:root {
--hue: 260;
--chroma: 0.2;
--color-50: oklch(97% calc(var(--chroma) * 0.1) var(--hue));
--color-100: oklch(93% calc(var(--chroma) * 0.2) var(--hue));
--color-200: oklch(85% calc(var(--chroma) * 0.4) var(--hue));
--color-300: oklch(75% calc(var(--chroma) * 0.6) var(--hue));
--color-400: oklch(65% calc(var(--chroma) * 0.8) var(--hue));
--color-500: oklch(55% var(--chroma) var(--hue));
--color-600: oklch(48% var(--chroma) var(--hue));
--color-700: oklch(40% calc(var(--chroma) * 0.9) var(--hue));
--color-800: oklch(32% calc(var(--chroma) * 0.8) var(--hue));
--color-900: oklch(25% calc(var(--chroma) * 0.7) var(--hue));
--color-950: oklch(18% calc(var(--chroma) * 0.5) var(--hue));
}Změňte --hue na 145 a máte zelenou paletu. Změňte na 25 a máte červenou. Kroky světlosti jsou perceptuálně rovnoměrné. Chroma se zužuje na extrémech, takže nejsvětlejší a nejtmavší odstíny nejsou přesycené. Toto je typ věci, který dříve vyžadoval designový nástroj nebo Sass funkci. Teď je to osm řádků CSS.
Animace řízené scrollem: Bez JavaScriptu#
Toto je vlastnost, která mi umožnila smazat nejvíce JavaScriptu. Animace propojené se scrollem — progress bary, parallax efekty, reveal animace, sticky hlavičky s přechody — dříve vyžadovaly IntersectionObserver, scroll event listenery nebo knihovnu jako GSAP. Teď je to CSS.
Indikátor průběhu scrollu#
Klasický "progress bar čtení" na vrcholu stránky článku:
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: oklch(55% 0.2 260);
transform-origin: left;
z-index: 1000;
animation: grow-progress linear both;
animation-timeline: scroll();
}
@keyframes grow-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}To je vše. Celý indikátor průběhu čtení. Žádný JavaScript. Žádný scroll event listener. Žádný requestAnimationFrame. Žádný výpočet "procenta odscrollování." Vazba animation-timeline: scroll() dělá vše.
Reveal při scrollu#
Prvky, které se plynule objevují, když vstupují do viewportu:
.reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}animation-timeline: view() váže animaci na viditelnost prvku ve viewportu. animation-range: entry 0% entry 100% znamená, že animace běží od okamžiku, kdy prvek začne vstupovat do viewportu, dokud není plně viditelný.
Parallax bez knihovny#
.parallax-bg {
animation: parallax linear both;
animation-timeline: scroll();
}
@keyframes parallax {
from {
transform: translateY(-20%);
}
to {
transform: translateY(20%);
}
}
.parallax-section {
overflow: hidden;
position: relative;
}Obrázek pozadí se pohybuje jinou rychlostí než scroll, čímž vytváří parallax efekt. Plynulý, výkonný (prohlížeč může kompozitovat na GPU) a nulový JavaScript.
Pojmenované scroll timelines#
Pro větší kontrolu můžete pojmenovat scroll timelines a odkazovat na ně z jiných prvků:
.scroller {
overflow-y: auto;
scroll-timeline-name: --my-scroller;
scroll-timeline-axis: block;
}
/* Prvek kdekoli v DOM může odkazovat na tuto timeline */
.indicator {
animation: progress linear both;
animation-timeline: --my-scroller;
}
@keyframes progress {
from { width: 0%; }
to { width: 100%; }
}Toto funguje, i když indikátor a scroller nejsou v relaci rodič/potomek. Jakýkoli prvek se může propojit s jakoukoli pojmenovanou scroll timeline. To je mocné pro dashboardová UI, kde scrollovatelný panel řídí indikátor v fixní hlavičce.
Anchor Positioning: Tooltipy a popovery správně#
Před anchor positioningem propojení tooltipu s jeho triggerovacím prvkem vyžadovalo JavaScript. Počítali jste pozice s getBoundingClientRect(), zpracovávali scroll offsety, spravovali kolize s viewportem a přepočítávali při změně velikosti. Knihovny jako Popper.js (nyní Floating UI) existovaly konkrétně proto, že to bylo tak těžké udělat správně.
CSS anchor positioning to dělá deklarativním:
.trigger {
anchor-name: --my-trigger;
}
.tooltip {
position: fixed;
position-anchor: --my-trigger;
/* Umístit horní střed tooltipu na spodní střed triggeru */
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
/* Záložní pozicování, pokud přeteče viewport */
position-try-fallbacks: flip-block, flip-inline;
}Automatické zpracování kolize s viewportem#
Vlastnost position-try-fallbacks je ta část, která by v JavaScriptu zabrala 200 řádků. Říká prohlížeči: "pokud tooltip přeteče viewport dole, překlopte ho nahoru. Pokud přeteče vpravo, překlopte ho doleva."
.dropdown-menu {
position: fixed;
position-anchor: --menu-button;
/* Výchozí: pod tlačítkem, zarovnané k levému okraji */
top: anchor(bottom);
left: anchor(left);
/* Pokud se nevejde dole, zkus nahoře. Pokud se nevejde zarovnáno vlevo, zkus vpravo */
position-try-fallbacks: flip-block, flip-inline;
/* Přidat mezeru mezi kotvu a dropdown */
margin-top: 4px;
}Pojmenované záložní pozice#
Pro větší kontrolu nad záložními pozicemi můžete definovat vlastní try options:
@position-try --above-right {
bottom: anchor(top);
right: anchor(right);
top: auto;
left: auto;
margin-top: 0;
margin-bottom: 4px;
}
@position-try --left-center {
right: anchor(left);
top: anchor(center);
left: auto;
bottom: auto;
translate: 0 -50%;
margin-top: 0;
margin-right: 4px;
}
.tooltip {
position: fixed;
position-anchor: --trigger;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
margin-top: 4px;
position-try-fallbacks: --above-right, --left-center;
}Prohlížeč zkouší každou záložní pozici v pořadí, dokud nenajde takovou, která udrží prvek uvnitř viewportu. Toto je druh prostorového uvažování, který byl v JavaScriptu skutečně bolestivý.
Kotvení s Popover API#
Anchor positioning se perfektně páruje s novým Popover API:
<button popovertarget="my-popover" style="anchor-name: --btn">Nastavení</button>
<div popover id="my-popover" style="
position-anchor: --btn;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
margin-top: 8px;
">
Obsah popoveru
</div>Žádný JavaScript pro zobrazení/skrytí (to řeší Popover API). Žádný JavaScript pro pozicování (to řeší anchor positioning). Žádný JavaScript pro chování light-dismiss (to řeší také Popover API). Celý vzor tooltip/popover/dropdown — vzor, který poháněl celé npm balíčky — je nyní HTML a CSS.
CSS Grid Subgrid: Vnořené zarovnání, které skutečně funguje#
Grid je mocný, ale měl jedno frustrující omezení: potomkový grid nemohl zarovnat své položky k rodičovskému gridu. Pokud jste měli řadu kartiček a chtěli, aby se nadpis, obsah a patička každé kartičky zarovnaly přes kartičky, měli jste smůlu. Interní grid každé kartičky byl nezávislý.
Subgrid to řeší.
Problém zarovnání kartiček#
/* Rodičovský grid */
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
/* Každá kartička se stane subgridem, zarovnávajícím řádky k rodiči */
.card {
display: grid;
grid-row: span 3; /* Kartička zabírá 3 řádky v rodiči */
grid-template-rows: subgrid;
gap: 0; /* Kartička řídí svou vlastní vnitřní mezeru */
}
.card-title {
font-weight: 600;
padding: 1rem 1rem 0.5rem;
}
.card-body {
padding: 0 1rem;
color: oklch(40% 0 0);
}
.card-footer {
padding: 0.5rem 1rem 1rem;
margin-top: auto;
border-top: 1px solid oklch(90% 0 0);
}Nyní se nadpisy přes všechny tři kartičky zarovnají na stejný řádek. Těla se zarovnají. Patičky se zarovnají. I když má jedna kartička dvouřádkový nadpis a jiná jednořádkový, zarovnání je udržováno rodičovským gridem.
Před Subgridem: Hacky#
Bez subgridu vyžadovalo dosažení tohoto zarovnání buď:
- Fixní výšky (křehké, rozbije se s dynamickým obsahem)
- JavaScript měření (pomalé, záblesk nezarovnání)
- Vzdání se a přijetí nezarovnání (časté, ošklivé)
/* Starý hack — křehký a rozbije se s dynamickým obsahem */
.card-title {
min-height: 3rem; /* modlete se, aby žádný nadpis toto nepřesáhl */
}
.card-body {
min-height: 8rem; /* ještě víc modlení */
}Subgrid dělá hack zbytečným. Rodičovský grid distribuuje výšky řádků na základě nejvyššího obsahu v každém řádku přes všechny kartičky.
Sloupcový Subgrid pro rozložení formulářů#
Subgrid funguje i na sloupcích, což je perfektní pro rozložení formulářů:
.form {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.75rem 1rem;
}
.form-row {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
align-items: center;
}
.form-row label {
grid-column: 1;
text-align: right;
font-weight: 500;
}
.form-row input {
grid-column: 2;
padding: 0.5rem;
border: 1px solid oklch(80% 0 0);
border-radius: 0.375rem;
}Všechny popisky se zarovnají. Všechny inputy se zarovnají. Sloupec popisků se automaticky přizpůsobí nejširšímu popisku. Žádné natvrdo zadané šířky.
View Transitions API: Navigace jako SPA bez frameworku#
Toto je nejambicióznější vlastnost na tomto seznamu. View Transitions API vám umožňuje animovat přechody mezi navigacemi stránek — včetně cross-document navigací (běžných kliknutí na odkazy v multi-page aplikaci). Váš statický HTML web může nyní mít plynulé, animované přechody stránek.
Cross-Document přechody#
Pro zapnutí cross-document view transitions přidáte jedno CSS pravidlo na starou i novou stránku:
@view-transition {
navigation: auto;
}To je vše. Prohlížeč nyní bude provádět plynulý přechod mezi stránkami při navigaci. Výchozí přechod je plynulé prolnutí opacity. Žádný JavaScript. Žádný framework. Jen dva řádky CSS na každé stránce.
Přizpůsobení přechodu#
Můžete přizpůsobit, jaké přechody se dějí, pojmenováním konkrétních prvků:
/* Na obou stránkách */
.page-header {
view-transition-name: header;
}
.main-content {
view-transition-name: content;
}
.hero-image {
view-transition-name: hero;
}
/* Animace přechodů */
::view-transition-old(content) {
animation: slide-out 0.3s ease-in both;
}
::view-transition-new(content) {
animation: slide-in 0.3s ease-out both;
}
::view-transition-old(hero) {
animation: fade-out 0.2s ease-in both;
}
::view-transition-new(hero) {
animation: none; /* Nový hero se prostě objeví */
}
@keyframes slide-out {
to {
transform: translateX(-100%);
opacity: 0;
}
}
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}Při navigaci mezi dvěma stránkami, které obě mají .hero-image s view-transition-name: hero, prohlížeč automaticky animuje obrázek z jeho pozice na staré stránce na jeho pozici na nové stránce. Je to vzor "shared element transition" z mobilního vývoje, nyní v prohlížeči.
SPA View Transitions#
Pro single-page aplikace (React, Vue, Svelte atd.) je JavaScript API přímočaré:
/* CSS strana — definujte animace přechodů */
::view-transition-old(root) {
animation: fade-out 0.2s ease-in;
}
::view-transition-new(root) {
animation: fade-in 0.2s ease-out;
}// JS strana — obalte aktualizaci DOM do startViewTransition
document.startViewTransition(() => {
// Aktualizujte DOM zde — React render, innerHTML swap atd.
updateContent(newPageData);
});Prohlížeč pořídí snímek starého stavu, spustí vaši aktualizaci, pořídí snímek nového stavu a animuje mezi nimi. Pojmenované prvky dostávají individuální přechody; vše ostatní dostane výchozí crossfade.
Respektování preferencí uživatelů#
Vždy respektujte prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}S čím můžete přestat#
Tyto vlastnosti kolektivně eliminují spoustu nástrojů, knihoven a vzorů, na které jsme roky spoléhali. Zde je to, co jsem odstranil nebo po čem jsem přestal sahat:
Sass/SCSS pro zanořování a proměnné#
CSS má nativní zanořování. CSS má custom properties (a má je už roky). Dva hlavní důvody, proč lidé sahali po Sassu, jsou nyní v jazyce. Pokud stále používáte Sass jen pro $variables a zanořování, můžete přestat.
/* Dříve: Sass */
$primary: #3b82f6;
.card {
background: white;
border: 1px solid lighten($primary, 40%);
&:hover {
border-color: $primary;
}
.title {
color: darken($primary, 15%);
}
}
/* Nyní: Nativní CSS */
.card {
--primary: oklch(55% 0.2 260);
background: white;
border: 1px solid color-mix(in oklch, var(--primary) 20%, white);
&:hover {
border-color: var(--primary);
}
.title {
color: color-mix(in oklch, var(--primary) 70%, black);
}
}CSS verze je mocnější — oklch dává perceptuálně uniformní manipulaci s barvami, color-mix funguje za běhu (Sass lighten je pouze v době kompilace) a custom properties mohou být dynamicky měněny z JavaScriptu nebo media queries.
JavaScript pro scroll animace#
Smažte svůj kód IntersectionObserver pro reveal-on-scroll. Smažte svůj JavaScript pro scroll progress bar. Smažte svůj parallax scroll handler. animation-timeline: scroll() a animation-timeline: view() zvládají všechny tyto případy s lepším výkonem (kompozitorové vlákno, ne hlavní vlákno).
// Dříve: JavaScript, který můžete smazat
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
document.querySelectorAll('.reveal').forEach((el) => observer.observe(el));/* Nyní: CSS, které nahrazuje vše výše */
.reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes reveal {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}Popper.js / Floating UI pro pozicování#
Pokud používáte Floating UI (dříve Popper.js) pouze pro základní pozicování tooltipů/popoverů, CSS anchor positioning ho nahrazuje. Přijdete o některé pokročilé funkce (virtuální prvky, vlastní middleware), ale pro 90 % případů — "umístti tento popover poblíž toho tlačítka a udrž ho ve viewportu" — to CSS dělá nativně.
Hacky s margin: auto pro centrování#
Toto není nové, ale stále vidím margin: 0 auto všude. Moderní layout nástroje to ve většině případů dělají zbytečným:
/* Starý způsob */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
/* Lépe: CSS Grid nebo Flexbox */
.page {
display: grid;
place-items: center;
}
/* Nebo: container queries dělají kontejner samotný responzivním */
.content {
container-type: inline-size;
width: min(100% - 2rem, 1200px);
margin-inline: auto; /* OK, stále auto, ale margin-inline je jasnější */
}margin: 0 auto stále funguje. Ale pokud ho používáte pro vertikální centrování nebo složité zarovnání, sáhněte raději po flexboxu nebo gridu.
Media Queries pro responzivitu na úrovni komponent#
Toto je to velké. Pokud píšete @media queries, abyste udělali komponentu responzivní, pravděpodobně to nyní děláte špatně. Container queries by měly být vaší výchozí volbou pro responzivitu na úrovni komponent. Vyhraďte @media queries pro rozhodnutí na úrovni stránky: změny rozložení, navigační vzory, tiskové styly.
/* Dříve: Media query pro stylování komponent (špatný rozsah) */
@media (min-width: 768px) {
.product-card {
flex-direction: row;
}
}
/* Nyní: Container query (správný rozsah) */
.product-card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.product-card {
flex-direction: row;
}
}Celkový obraz#
Tyto vlastnosti neexistují izolovaně. Skládají se. Container queries + :has() + nesting + oklch() + layers — použité společně — vám dávají zkušenost s psaním CSS, která by byla nepoznatelná před pěti lety:
@layer components {
.card-wrapper {
container-type: inline-size;
}
.card {
--accent: oklch(55% 0.2 260);
display: grid;
gap: 1rem;
padding: 1.5rem;
border-radius: 0.5rem;
background: white;
border: 1px solid oklch(90% 0 0);
&:has(img) {
padding-top: 0;
img {
border-radius: 0.5rem 0.5rem 0 0;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
}
&:has(.badge.new) {
border-color: var(--accent);
}
.title {
font-size: 1.125rem;
font-weight: 600;
color: oklch(20% 0 0);
text-wrap: balance;
}
.meta {
font-size: 0.875rem;
color: oklch(50% 0 0);
}
@container (min-width: 500px) {
grid-template-columns: 200px 1fr;
&:has(img) {
padding: 0;
img {
border-radius: 0.5rem 0 0 0.5rem;
height: 100%;
aspect-ratio: auto;
}
}
.content {
padding: 1.5rem;
}
}
}
}Tento jediný blok zpracovává:
- Responzivní rozložení na úrovni komponent (container queries)
- Podmíněné stylování na základě obsahu (
:has()) - Čistou organizaci selektorů (nesting)
- Perceptuálně uniformní barvy (
oklch) - Předvídatelnou správu specificity (
@layer)
Žádný preprocesor. Žádný CSS-in-JS runtime. Žádné utilitní třídy pro toto. Jen CSS, dělající to, co CSS mělo dělat celou dobu.
Platforma doběhla. Konečně. A stálo to za čekání.