Taky máte v CSS rádi jednotku vw? Volkswagen mezi jednotkami? A víte, že je úplně, ale úplně nahovno?

Výraz 100vw znamená sto procent šířky viewportu. Celá plocha prohlížeče. Chcete, aby váš full-bleed cover měl přesně tuhle šířku? Stačí mu nastavit width: 100vw.

A co když má stránka svislý scrollbar? Uživatelé Maců ho moc nevnímají, protože macOS ho při nečinnosti automaticky schovává. Windows ho zobrazují trvale. Jde o (obvykle) 15pixelový pruh vpravo, který do šířky viewportu logicky nepatří. Nic v něm nevykreslíte. Naopak, prostor viewportu ubírá.

Jenže 100vw je šířka včetně scrollbaru!

Co to znamená? Vyrobíte si na Macu full-bleed cover, width: 100vw, otevřete to v Chrome na Windows a hele, stránka horizontálně scrolluje. Cover přečnívá za pravý okraj, přesně o těch 15 pixelů scrollbaru.

Základní jednotka pro šířku viewportu, která nerespektuje šířku viewportu.

Tenhle bug je tu už dlouho a nikdo s ním nehne. Tiket v CSS Working Group visí v limbu od roku 2021 a podobné diskuze běží ještě déle. Browser vendors říkají „spec to nařizuje“, spec autoři říkají „browsery to interpretují špatně“ a webový vývojář mezitím píše workaround na úplně banální full-bleed obrázek.

Mezitím přibyly další jednotky (svw, lvw, dvw) a žádná z nich scrollbar nezohledňuje. Všechny nové jednotky jsou na hovno.

Jak tedy ten full-bleed cover udělat, aby se na Windows nezadělalo na horizontální scroll? Workaroundů existuje hned několik. Pojďme si je projít od toho nejhloupějšího.

Slepé uličky

První instinkt: vynutit svislý scrollbar trvale a odečíst jeho šířku.

html {
	overflow-y: scroll;
}

.cover {
	width: calc(100vw - 15px); // NEFUNGUJE
}

Tohle by fungovalo, pokud by scrollbar měl skutečně 15px. Což nemá na Macu, mobilech (0px) a uživatelů s vlastním stylováním.

Druhý pokus: scrollbar-gutter: stable. Říká prohlížeči „vždycky rezervuj místo na scrollbar, ať už ho zobrazíš, nebo ne“. Konzistentní layout, žádné poskakování při přepínání mezi krátkou a dlouhou stránkou. Jenže 100vw to neopraví – ten zahrnuje šířku scrollbaru bez ohledu na to, jestli je vidět.

Závěr: šířku scrollbaru musíme změřit.

Řešení 1: změřit ho JavaScriptem

Property document.documentElement.clientWidth vrací vnitřní šířku HTML elementu a scrollbar do ní nezahrnuje. Propíšeme ji do CSS proměnné a aktualizujeme při každém resize:

<script>
new ResizeObserver(() => {
	document.documentElement.style.setProperty(
		'--viewport-width',
		document.documentElement.clientWidth + 'px'
	);
}).observe(document.documentElement);
</script>

A pak jen doplníme do CSS defaultní hodnotu a můžeme proměnnou využívat:

:root {
	--viewport-width: 100vw;  // default
}

.cover {
	width: var(--viewport-width);
}

Funguje na všem. Triggne se i v okamžiku, kdy scrollbar uprostřed života stránky náhle vznikne nebo zmizí. Třeba když uživatel rozbalí dlouhý seznam. Pár řádků JS a hotovo.

Existuje ale i čistě CSS cesta.

Řešení 2: zapomeňte na vw, máte cqw

V roce 2022 přišly do CSS container query units. 100cqw znamená „100 % inline-size nejbližšího container-query kontejneru“. Stačí jako container určit body, jehož šířka scrollbar nezahrnuje:

body {
	container-type: inline-size;
}

.cover {
	width: 100cqw;
}

Žádný workaround, žádný horizontální scroll. Prostě jednotka, která dělá co slibuje.

Háček: vnořené containery

Tohle platí jen pokud body zůstane jediný container-query kontejner v subtree. Jakmile někde uvnitř použijete container-type pro komponentu (typicky abyste měli responsive komponenty nezávislé na šířce okna), cqw uvnitř té komponenty se začne vztahovat k té komponentě, ne k body.

Většině webů to nevadí. Container queries na komponenty klidně mít můžete, jen v nich obvykle nepotřebujete full-bleed cover. Ten patří do hlavního layoutu, ne do karty produktu nebo sidebaru.

Pokud ale stavíte opravdu obecný framework, kde si full-bleed může vyžádat libovolná komponenta kdekoliv, omezení to být může. A pro ten případ je tu poslední finta.

Řešení 3: registrovaná @property

Vyrobíme si CSS proměnnou typu length. Důležité slovo: typu. Registrovaná @property se počítá jako length už při deklaraci a dál se dědí jako konkrétní pixely – nezávisle na tom, kolik containerů kdo cestou nasázel:

@property --viewport-width {
	syntax: '<length>';
	inherits: true;
	initial-value: 0px;
}

html {
	container-type: inline-size;
}

body {
	--viewport-width: 100cqw;
}

.cover {
	width: var(--viewport-width);
}

Bez registrace by to nepomohlo, protože proměnná by se substituovala textově a 100cqw by se resolvlo až v místě použití, tedy v nejbližším containeru. S registrací typu length se hodnota zmrazí a dědí se nezávisle.

Až jednou někdo zavře ten tiket v CSS WG, přijde sluneční den. Do té doby zapomeňte, že vw existuje. 🙂