Jak vytvořit tmavý režim / dark mode v CSS

V roce 2019 přišel Apple s tmavým režimem v iPhonech. To tmavé režimy značně zpopularisovalo.

Uživatelé si najednou nastavují tmavé zobrazení a očekávají, že se tomu přizpůsobí i web.

Tmavý, nebo světlý?

Jestli je lepší, čitelnější nebo pro oči lepší tmavý/světlý web je těžká otázka. Zdá se, že každému vyhovuje něco jiného.

Zastánci tmavých režimů mají pocit, že jim světlý web vypálí oči. Zastánci světlých zase argumentují lepší čitelností při intensivním okolním světle.

A potom je tu skupina těch, kteří preferují automatické nastavení podle denní doby.

Naštěstí se vhodným řešením dá zavděčit všem…

Nastavení v systému/prohlížeči

Lze se setkat se třemi možnostmi nastavení:

  1. světlé,
  2. tmavé,
  3. automatické

Automaticky znamená, že se podle denní doby (většinou východ/západ slunce) režim změní.

Ve Windows jde nastavit pouze tmavý nebo světlý vzhled aplikace. Nastavení → Přizpůsobení → Barvy → Zvolit výchozí režim aplikace:

Nastavení tmavého režimu ve Windows

Toto nastavení potom přebírají prohlížeče.

V macOS existuje i automatický režim.

Nastavení tmavého režimu v macOS

V iOS potom obdobně:

Nastavení tmavého režimu v iOS

Web podporující změnu režimu dle nastavení systému by se při změně měl ihned přebarvit.

Světlé nebo tmavé zobrazení jde obvykle nastavit i přímo v prohlížeči.

Detekce v CSS @media pravidlu

Přímo v CSS existuje @media pravidlo detekující preferovaný režim.

Tmavý režim

@media (prefers-color-scheme: dark) {
  /* styly pro tmavý režim */
}

Světlý režim

@media (prefers-color-scheme: light) {
  /* styly pro světlý režim */
}

Podpora v prohlížečích je slušná. Chybí akorát v marginálních prohlížečích jako je IE 11 nebo starý Edge.

Pokud se ale veškeré styly neuzavřou do těchto podmínek, ale jeden z nich se nechá mimo, zůstane v nepodporovaných prohlížečích alespoň výchozí styl.

Zjištění podpory v JS

Detekce v JavaScriptu lze potom provést stejnými media pravidly v matchMedia:

if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  // uživatel preferuje tmavý režim
}
else {
  // uživatel preferuje světlý režim
}

Hodí se i detekce, jestli prohlížeč dokáže převzít informaci o nastavení ze systému/prohlížeče.

if (window.matchMedia('(prefers-color-scheme)').media === 'not all') { 
  // tmavý/světlý režim není podporován
}

A na základě toho (ne)zobrazovat přepínání mezi světlým a tmavým tématem.

Reakce na změnu režimu

Na změnu režimu v systému/prohlížeči lze reagovat následovně:

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
  if (event.matches) {
    // přepnuto na tmavý
  }
  else {
    // přepnuto na světlý
  }
})

Pro Safari je v roce 2020 potřeba volit jiný postup a použít addListener bez názvu události, který funguje i v ostatních prohlížečích:

window.matchMedia('(prefers-color-scheme: dark)').addListener(event => {
  if (event.matches) {
    // přepnuto na tmavý
  }
  else {
    // přepnuto na světlý
  }
})

Tato událost se zavolá ve 2 případech:

  1. Uživatel si přepne v nastavení systému/prohlížeče preferovaný režim.
  2. Režim se změní sám (např. automatická změna v iOS podle denní doby).

Best practice

Jak ale v praxi nejlépe tmavý režim vytvořit?

Nejjednodušší řešení je přidat pár řádků kódu, které pomocí CSS filtrů otočí barvy:

@media (prefers-color-scheme: dark) {
  html {
    background: #1D1D1D;
    filter: invert(100%) contrast(90%) hue-rotate(180deg);
  }
}

Pro seriosní použití to ale moc není :–)

Když pominu to, že některé barvy a obrázky nedopadnou takovou změnou dobře, další věc je, že nestačí jen invertovat barvy.

Barvy totiž slouží i k znázornění důležitosti a fakt, že jde zdůraznit část stránky použitím světlejší barvy, platí stejně u tmavého i světlého webu stejně – tato logika není invertovaná.

Proto je pro dobrý výsledek dobré barvy nastavit ručně.

CSS proměnné

Pro pořádné vytvoření tmavého režimu (nebo obecně snadné změny stylu) je ideální používat CSS proměnné.

Pokud se tmavý režim doplňuje do již hotového webu, je vhodné na ně přejít. Použití je potom následující:

:root {
  --background-color: #ededed;
  --text-color: #212121;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background-color: #111;
    --text-color: #ededed;
  }
}

body {
  background-color: var(--background-color);
  color: var(--text-color);
}
  1. Nejprve se nadeklarují výchozí proměnné světlého režimu pro selektor :root.

    Bývá zvykem pro CSS proměnné používat kořenový selektor :root, ten u HTML stránky odpovídá selektoru html, jen je silnější (ukázka).

    Teoreticky je universálnější – třeba kdyby se styly potom připojily do <svg>, kde žádný <html> element není. V praxi se tedy může použít klidně i html.

  2. Potom se pro preferovaný tmavý režim proměnné přepíší na tmavé varianty.

  3. Nyní se všude v kódu pro barvy používají CSS proměnné.

Převod SASS proměnných

Pokud se na webu používaly proměnné v preprocesoru (např. SASS):

a {
  color: $color-link;
}

Je možné je převést na použití var(--color-link) nahrazením tohoto regulárního výrazu \$(color[\w-]*) na var(--$1).

Co s rgba()

V případě preprocesorů je běžné psát pro průhledné barvy:

a {
  color: rgba($color-link, .7);
}

S CSS proměnnou nastane problém, protože tohle na ní nejde aplikovat. Řešení je třeba převod do RGB formátu. Jde k tomu použít jednoduchý SASS mixin. A proměnnou barvy potom použít uvnitř rgba:

@function hexToRgb($color) {
    @return red($color), green($color), blue($color);
}
:root {
  --color-rgb-background: #{hexToRgb(#666)};
}
div {
  background: rgba(var(--color-rgb-background), .5);
}

Fallback CSS proměnných

Protože třeba v IE 11 CSS proměnné nefungují, hodí se použít nějaký fallback. Existuje šikovný PostCSS plugin postcss-css-variables. Ten dokáže vlastnosti s proměnnými zduplikovat a var(--color-neco) nahradit skutečnou hodnotou.

postcss([
  cssvariables({
    preserve: true,
  }),
])

Hodit se může i možnost nakonfigurovat proměnné přímo:

postcss([
  cssvariables({
    preserve: true,
    variables: {'--color-background', '#111')},
    preserveInjectedVariables: false,
  }),
])

Výsledkem je potom následující kód:

neco {
  background: #111;
  background: var(--color-background);
}

Nové prohlížeče použijí proměnnou, staré fallback.

Ruční přepínání

Říká se, že každá položka v nastavení je selhání designéra. Z tohoto pohledu se nabízí žádnou možnost přepínání mezi vzhledy nemít a nechat to na nastavení systému/prohlížeče.

Je otázka, proč by člověku, co má v systému nastaven tmavý režim, vadilo, že se tomu přizpůsobí web.

Nabízí se jen situace, kdy jsou uživatelé zvyklí na světlý web a najednou se jim přepne do tmavé podoby.

Pro možnost autodetekce i ručního přepínání zároveň je vhodné použít nějaký mixin vkládající kód pro vynucený i automatický tmavý režim:

@mixin inDark {
    .theme-dark {
        @content;
    }

    .theme-system {
        @media (prefers-color-scheme: dark) {
            @content;
        }
    }
}

A použití:

@include inDark {
    --color-background: #111;
}

To zajistí následující chování:

  1. Bude-li mít značka <html> třídu theme-dark, nastaví se tmavé proměnné.

  2. Bude-li mít značka <html> třídu theme-system, nastaví se tmavé proměnné jen když bude uživatel preferovat tmavý režim.

  3. Nebude-li mít <html> žádnou třídu, použijí se výchozí světlé barvy.

Nyní stačí už jen přepínat třídy JavaScriptem.

Duplicitní deklarace barev

Jak je vidět z kódu, deklarace CSS proměnných bude v kódu dvakrát. Mohlo by se nabízet detekovat styl čistě v JS a až podle toho třídu nastavovat. Bohužel to má nevýhodu v problikávání při načítání, než se stihne JS spustit.

Takže jedině takový JS nedávat do externího souboru, ale umístit ho do kódu před samotný obsah webu.

Uložení nastavení

K úvaze je, kam uložit nastavení vzhledu. Záleží na typu aplikace. U klasické aplikace renderující se na straně serveru jsou výhodnější cookies než třeba localStorage, protože kvůli prevenci probliknutí po změně je nutné znát situaci už na backendu.

U SPA je localStorage v pohodě.

Nastavení se i může ukládat do profilu, pokud se uživatel přihlašuje.

Řešení obvyklých problémů

Při převádění již existujícího světlého webu do tmavých barev dost možná narazíte na následující problémy:

Barvy ikon a obrázků

Asi nejpracnější část výroby tmavého režimu, pokud nejsou ikony řešeny vhodným způsobem.

Ideální je používat nějaké ikony, které jde snadno přebarvovat. Takže třeba SVG vložené inline do HTML kódu nebo pomocí <use> z SVG spritu:

<svg>
  <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="sprite.svg#icon"></use>
</svg>

A v ikoně používat pro přebarvitelné části v atributech fill a stroke hodnotu currentColor. Šlo by i používat přímo CSS proměnné, ale byl by s nim problém kvůli nepodpoře ve starších prohlížečích.

Potom už jde ikonky snadno přebarvit:

svg {
  fill: var(--color-text);
}

Dobře přebarvitelné jsou i staré dobré font ikony, ale ty trpí řadou různých problémů, že je používat nedoporučuji.

Přebarvení CSS filtrem

U jiných než vektorových obrázků (PNG, JPG, GIF) je jediná možnost použít již zmíněnou vlastnost filter.

Průhledný obrázek jako třeba logo jde převést na bílou následovně:

.theme-dark .logo {
  filter: invert(.5) brightness(2);
}

Případně obráceně na černou:

.theme-dark .logo {
  filter: invert(.5) brightness(0);
}

Velký počet barev

Hodně webů trpí nešvarem v podobě velkého počtu barevných odstínů. Většinou to vzniká tak, že se barvy chaoticky střílí od oka. Ve finále jsou potom na webu vysoké desítky až stovky různých barev.

Je dobré se podívat na všechny použité barvy – třeba nástrojem CSS Color Extractor – a pokusit se je sjednotit.

Usnadní to práci a udržování dvou (popř. více) barevných schémat.

Barva prohlížeče theme-color

Zvlášť na mobilech je populární přebarvovat horní lištu.

<meta name="theme-color" content="#1081DD">

I tuto lištu se pravděpodobně hodí barvit podle použitého režimu, aby ladila ke zbytku stránky.

Není problém barvu přepínat JavaScriptem, takže může reagovat i na přepínání a detekci režimu.

Akorát to nejde jen v CSS, ale musí se skriptem měnit atribut contentživá ukázka.

Jak je přechod náročný?

Ačkoliv to tak může vypadat, vytvořit tmavý režim k již existujícímu světlému webu není úplně práce na odpoledne. A může vyžadovat velkou porci změn ve zdrojovém kódu.

Příklad z jednoho projektu:

Změny kvůli tmavému režimu

Kde se inspirovat

Jaké zvolit odstíny černé a šedé? Příklad populárních webů a aplikací, kde existuje světlý/tmavý režim.

Odkazy jinam

To je všechno. Líbil se vám článek a chcete se dozvědět, až vyjde další?

Sledujte:

 

Připomínky mi pište do komentářů ↓

CSS mapa s popisky

Body s popisky na mapě

Statická obrázková mapa s vlastními body a :hover popisky.

Zvýraznění aktivní kotvy

Zvýraznění aktivované kotvy (:target)

Pokud se v rámci stránky používají odkazy na jednotlivé #části, může být vhodné zvýrazněním ukázat, kam odkaz mířil.

Baterka v CSS a JavaScriptu

Vytvoření efektu baterky v CSS a JS

Jak jednoduše vytvořit na stránce efekt baterky? Tedy ztmavit web a prohlížet ho jakýmsi průzorem.

Kreslení v CSS

„Kreslení“ pomocí CSS

Jak vytvářet jednoduché tvary místo obrázků prostým CSS?

Načtení obrázku, až když je potřeba

Zpožděné načtení obrázku, až když je potřeba

Kromě potřeby nahrát obrázek dopředu (preload), aby byl v době použití 100% připravený, může být potřeba opačná – načíst jej, až v momentě, kdy je potřeba. Z důvodu nemrhání datovým přenosem.

Komentáře