Proměnné v CSS již dnes

Možnost používat CSS proměnné (někdy známé jako CSS Makra) je starý požadavek, který se do teď řešil různými generátory jako jsou SASS nebo LESS. Ty ale mají tu nevýhodu, že stále generují statické definice, které nelze (jednoduše) měnit za běhu programu (tedy na straně klienta).

W3C ale již pracuje na nové specifikaci CSS Custom Properties for Cascading Variables, která umožní využívání proměnných a dokonce i jejich snadnou změnu na straně klienta a tedy dynamickou hromadnou změnu stylů.

A i když jde stále jen o návrh, proměnné můžete již nějakou dobu používat ve Firefoxu (plná podpora v 42+) a také nově v Chrome 49+, Safari 9.1+ a iOS 9.3+. Microsoft podporu zvažuje.

UPDATE: V Edge 15 (Windows Creators Edition) jsou Custom properties plně podporovány. Jediný (stále používaný) prohlížeč, který je tedy neumí, je IE11 (podle statistik v době vydání Creators edition používá IE11 dvakrát tolik lidí než Edge).

Definice proměnné

V CSS můžete proměnné definovat stejně jako CSS pravidla. Aby bylo možno proměnné odlišit od základních pravidel, začínají proměnné dvěma pomlčkami (podobně jako prefixy začínají jednou pomlčkou):

:root { /* odpovídá nodu html */
    --text-color: black;
    --bg-color: white;
}

Poznámka: Záměrně nebyla přejata definice z LESS ani SCSS, aby bylo možno v těchto prekompilátorech odlišit, které proměnné se zkompilují (tzn. převedou se na absolutní hodnoty) a které zůstanou proměnné (tedy změny-schopné) i za běhu.

Proměnné jsou kaskádové stejně jako styly, takže můžete pro jednotlivé selektory nadefinovat různé hodnoty a ty se pak použijí místo těch, které jsou výchozí. V opačném případě se proměnné dědí:

.error {
    --text-color: red;
    --bg-color: yellow;
    font-weight: bold;
}
.note {
    --text-color: gray;
    font-style: italic;
}

Použití proměnných

Použití není tak přímočaré, protože místo abyste použili přímo jméno proměnné, musíte použít funkci var():

p {
    color: var(--text-color);
    background: var(--bg-color)
}

Důvodem použití funkce je fakt, že v definici můžete uvést výchozí hodnoty pro případ, že daná proměnná neexistuje nebo neobsahuje správný formát pro daný styl:

p {
    color: var(--text-color, black);
    background: var(--bg-color, white)
}
.error {
    --bg: url(err.jpg) red;
    background-color: var(--bg, red);
    /* barva se nastaví na červenou,
     i když proměnná obsahuje hodnotu
     nevhodnou pro danou definici */
}

Tím odpadá potřeba definovat výchozí hodnoty proměnných a lze je využít jen v případě, že je potřebujete změnit. Také je to výhodné pro responzivní layout, který pomocí LESS nebo SASS nebyl možný:

img {
    margin: var(--img-margin, 0 0.5em);
    width: var(--img-width, 100%);
}
@media (min-width: 400px) {
    img {
        --img-margin: 1em 2em;
        --img-width: 50%;
    }
}

Výše uvedený kód nastaví šířku obrázku na 100%. Na větších displejích ale změní šířku na 50% a také zvětší okraje.

Také si všimněte, že proměnné i výchozí hodnoty mohou obsahovat definice pro hromadná pravidla jako jsou margin nebo background, které obsahují více hodnot. Problém nedělá ani to, když defaultní hodnota obsahuje čárku jako třeba v případě font-family, kde uvádíte jména písem oddělených čárkou (prohlížeč bere ve funkci var() v úvahu jen první čárku jako oddělovač, ostatní jsou již součástí definice).

p {
    font-family: var(--font, Times, serif);
}
.code {
    --font: "Courier New", monospace;    
}

Proměnné můžete používat i ve výpočtech a nebo naopak vypočítávat hodnoty proměnných:

img {
    margin: calc(var(--scale), 1) * 1em);
    width: calc(100% / var(--scale, 1)) ;
}
@media (min-width: 400px) {
    img {
        --scale: 2;
        /* margin bude 2em, a šířka 50% */
    }
}
p {
    --font-size: calc(var(--scale) * 12pt);
}

Výpočet je potřeba použít i v případě, že vlastně nechcete nic počítat, ale jen potřebuje získat správné jednotky. Když totiž použijete proměnnou obsahující číslo…

p { /* nebude fungovat! */
    font-size: var(--scale, 1em);
    line-height: var(--scale, 1);
    border: solid var(--scale, 1px);
}
@media (min-width: 720px) {
    :root { --scale: 1.2; }
}

… narazíte na problém, že definice font-size: 1.2 a border: 1.2 nejsou platná (i když line-height: 1.2 platné je). Proto je v tomto případě potřeba definici písma a rámečku vynásobit jedničkou se správnými jednotkami:

p {
    font-size: calc(
        var(--scale, 1) * 1em);
    line-height: var(--scale, 1);
    border: solid calc(
        var(--scale, 1) * 1px);
}
@media (min-width: 720px) {
    :root { --scale: 1.2; }
}

Výsledek bude tedy font-size: 1.2em a border: 1.2px, což je platné.

Ještě jedno upozornění: jména proměnných jsou case-sensitive, takže si dejte pozor na pojmenování. Hodnota --font-size není to samé jako --Font-Size ani jako --fontSize (což je javascriptová alternativa CSS jmen).

Hodnota proměnné

Proměnné mohou obsahovat cokoliv, co je validní CSS hodnota, tedy čísla, písmena a další znaky (např. %) a také je možné jejich hodnotu vypočítat funkcemi jako je calc(). Hodnota proměnné končí znakem středníku „;“ nebo složené závorky „}“ stejně jako normální pravidla. U funkce var() končí defaultní hodnota odpovídající pravou závorkou.

Proměnná se nevytvoří nebo nezmění, pokud její hodnota obsahuje chybu, např. nespárovanou závorku nebo uvozovku, vykřičník bez slova important nebo jiný nepodporovaný znak.

:root {
    --var1: url('a'; /* chybí závorka */
    --var2: {yellow; /* chybí závorka */
    --var3: 'Value:; /* chybí uvozovka */
}

Chybný je také cyklický odkaz na proměnné:

:root {
    /* nelze vytvořit ani jednu */
    --a: calc(var(--b) * 2);
    --b: calc(var(--a) / 2);
    --c: calc(var(--c) * 1);
}

Nejde ale o cyklický odkaz, pokud jsou proměnné vnořené, protože pak se použije zděděná hodnota:

p {
    --scale: 1;
}
p > a {
    --scale: calc(var(--scale) * 1.2);
}
p > a > em {
    --scale: calc(var(--scale) * 0.64);
  }     /* scale = 0.8 */

Proměnná může obsahovat i texty, které jsou z pohledu CSS nesmyslné, ale lze je využít v JS po přečtení hodnoty:

:root {
    --calculus: Math.round(
          window.innerWidth / 2);
}
var result = eval($(document)
  //getCssProperty je smyšlená funkce
    .getCssProperty('--calculus'));
$('body').css('width', result);

Přenos hodnoty

Jak už bylo řečeno výše, hodnotu proměnné můžete buď přímo zadat nebo vypočítat pomocí calc():

p {
    --color: black;
    --padding: calc(var(--scale) * 10px);
}

Co ale nemuselo být zcela jasné je to, že hodnota proměnné může být převzata z jiné proměnné:

p {
    --color: var(--theme-text-color);
    --padding: var(--block-padding);
}

V tomto případě je potřeba si uvědomit, jak se taková hodnota používá.

Programátoři by se totiž mohli domnívat, že přiřazení se provede při načtení pravidla – podobně, jako je tomu např. v JavaScriptu:

var
  //výchozí hodnoty stylu
  theme-text-color = 'black',
  block-padding = 10,

  //převzaté hodnoty
  color = theme-text-color,
  padding = block-padding;

//teď mají color a theme-text-color
//stejnou hodnotu

theme-text-color = 'blue';
block-padding = 0;

//teď se změnily výchozí hodnoty,
//ale převzaté hodnoty pořád obsahují
//color = 'black' a padding = 10

color = theme-text-color;

//teď se aktualizovala převzatá hodnota 
//color, ale padding je pořád 10

V JavaScriptu, PHP, C++, apod. se přiřazení provede pouze jednou a poté zůstává hodnota neměnná až do další změny.

V CSS se ale neprovádí jednorázové přiřazení, ale při každé změně dojde k přepočtu všech hodnot a výpočtů:

:root {
    --theme-text-color: green;
    --scale: 1;
p {
    --color: var(--theme-text-color, blue);
    --padding: calc(var(--scale, 1) * 10px);
}

@media (min-width=400px) {
    :root { scale: 1.5; }
}
@media (min-width=600px) {
    :root { scale: 2; }
}
@media print { /* černobílí tisk */
    :root { --theme-text-color: black; }
}

Pokud nadefinujete tento styl, znamená to, že při každé změně rozlišení (např. otočení telefonu nebo změny okna prohlížeče) dojde k přepočtu paddingu, protože se změní hodnota scale. Stejně tak v okamžiku, kdy uživatel vytiskne stránku, přepočte se hodnota color na black, protože se změní výchozí hodnota theme-text-color.

V programovacích jazycích to odpovídá tomu když nadefinujete listener na změnu hodnoty:

var
  //výchozí hodnoty stylu
  theme-text-color = new Color('black'),
  block-padding = new Number(10),

  //převzaté hodnoty
  color = new Color(theme-text-color),
  padding = new Number(block-padding);

//teď mají color a theme-text-color
//stejnou hodnotu

theme-text-color.on('change', function(c) {
    color.setColor(c);
});
block-padding.on('change', function(i) {
    padding.setValue(i);
});

//od teď se bude color a padding měnit
//společně s výchozími hodnotami

theme-text-color.setColor('blue');
block-padding.setValue(0);

//teď se změnily výchozí hodnoty,
//a pomocí listenerů i převzaté hodnoty

U CSS proměnných se totiž ve skutečnosti neprovádí přiřazení hodnoty, ale pouze se ukládá to, co zadáte. Teprve v okamžiku, kdy prohlížeč potřebuje použít pravidlo pro vykreslení prvku, provede jejich vyhodnocení a nahradí všechna var() za skutečné hodnoty (stejně jako vypočte všechny calc()):

p {
    --theme-text-color: blue;
    --color: var(--theme-text-color);
    color: var(--color, black);
}

V tomto případě obsahuje proměnná theme-text-color hodnotu "blue" (tedy řetězec s 4 znaky B, L, U a E), ale proměnná color neobsahuje "blue" ale hodnotu "var(--theme-text-color)" (tedy opět řetězec). Teprve při vykreslení odstavce p prohlížeč zjistí, že barva je definována jako "var(--color)", a tedy musí přečíst proměnnou color. Na základě jejího obsahu "var(--theme-text-color)" se podívá na proměnnou theme-text-color, která obsahuje "blue". Jelikož "blue" je platná hodnota pro barvu, použije ji prohlížeč při vykreslení.

Použití proměnných v JS

Samozřejmě proměnné můžete číst v Javascriptu pomocí  funkcí getComputedStyle() a getPropertyValue() a i je měnit funkcí style.setProperty():

var
    p = '--text-color',
    doc = document.documentElement,
    styles = getComputedStyles(doc),
    color = String(styles
        .getPropertyValue(p)
       ).trim();

    doc.style.setProperty(p, 'black');

Funkce getPropertyValue() vrací hodnoty přesně tak, jak je uvedena v CSS souboru včetně mezer a jednotek. Proto je potřeba hodnotu zbavit nepotřebných znaků (String::trim()) nebo převést na číslo (parseInt()). Naopak toho ale můžete využít k tomu, že uložená hodnota nebude dávat smysl pro CSS, ale pro jiný jazyk (např. Javascript nebo jQuery) tak, jak je uvedeno výše.

Při změně hodnoty pak nezapomeňte uvést potřebné jednotky:

var 
    doc = document.documentElement,
    dpr = window.devicePixelRatio,
    f = dpr > 2 ? 12 : 10,
    w = Math.round(320 / doc.clientWidth); 

doc.style.setProperty('--font', f + 'pt');
doc.style.setProperty('--width', w + '%'); 

Zpětná kompatibilita

Samozřejmě stále je potřeba myslet na zpětnou kompatibilitu, což trochu ztěžuje použití proměnných např. v responzivním layoutu:

img {
    margin: 1em;
    margin: calc(var(--scale), 1) * 1em);
    width: 100%;
    width: calc(100% / var(--scale, 1)) ;
}
@media (min-width: 400px) {
    img {
        --scale: 2;
        /* margin bude 2em a šířka 50% */
        /* starý prohlížeč ale stále
           použije 1em a 100% */
    }
    img.old {
        margin: 2em;
        width: 50%;
        --scale: 2;
        /* margin bude 2em a šířka 50%
         ale scale se nepoužije
         protože se přepsala pravidla,
         která ho používají */
    }
}

Ostatní návrhy

V původních návrzích W3C se uvažovalo i o pojmenování proměnných začínajících var-* (např. var-text-color), ale tam by byl problém při čtení var(var-text-color, black), kdy se v textu opakuje var a hrozí riziko, že se na jedno z nich zapomene. Tato implementace byla ve Firefoxu 29 a v Chrome 33 (jako -webkit-var-*) a následně byla odebrána (Firefox 30 a Chrome 34 proměnné opět nepodporují).

Taktéž byl návrh používat pojmenování s $ ve stylu PHP (např. $textColor), ale použití znaku $ si W3C schovalo na později pro plánovaná makra (ve stylu C/C++ maker):

/* Toto zatím nikde nefunguje!!! */
$box {
    display: block;
    margin: 0 auto;
    padding: 1em;
}
/* přiřazení přes pravidlo */
p.block {
    matches: $box;
    border: 1px solid black;
}
/* přiřazení přes zavináč */
@matches $box p.block, div;
@matches .article > p $box;
/* díky $ nezáleží na pořadí */

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *