Barvy & design tokeny

Snažil jsem se dát do kupy novou architekturu pro barvy resp. design tokeny, která by byla dobře škálovatelná a jednoduše přebarvitelná pro jednotlivé instance či embedy.

Základem je palette, což jsou definice základních barev pro daný theme. Jednalo by se vždy o soubor v /src/themes/{instance}.css, tedy např. /src/themes/www.volebnikalkulacka.cz.css. Jde o obyčejné CSS proměnné, takže je případně možné je definovat/přepsat i na úrovni mimo design systém.

:root {
  --palette-primary: blue;
  --palette-secondary: red;
  --palette-neutral: gray;
  --palette-yellow: yellow;
}

Pak nastupují definice design tokenů v Tailwindu. Design tokeny přiřazují jednotlivým barvám sémantický význam.

Struktura názvu design tokenů je (pro barvy) je --color-{element}-{varianta}-{stav}, např .--color-bg-primary nebo --color-fill-neutral-hover. Kromě toho jsou definovány ještě kombinace {element}-on-{token}, např. --color-text-on-bg-secondary.

Hodnoty tokenů jsou pomocí OKLCH odvozeny z palette barev. Zároveň ale vždy upřednostňují CSS proměnnou, pokud je definovaná, pokud si tedy budu chtít s barvami víc vyhrát a nějakou si přepsat, budu to moci udělat.

Kromě toho jsou pomocí CSS funkce light-dark() rovnou definovány barvy pro dark mode.

Tedy /src/styles.css bude vypadat nějak následovně:

@theme {
  --color-bg-primary: light-dark(
    var(
      --bg-primary-light,
      oklch(from var(--palette-primary) calc(l + 0.1) calc(l - 0.2) h)
    ),
    var(
      --bg-primary-dark,
      oklch(from var(--palette-primary) calc(l - 0.4) calc(l - 0.2) h)
    )
  );

  --color-text-on-bg-primary: light-dark(
    var(
      --text-on-bg-primary-light,
      oklch(from var(--palette-primary) calc(l - 0.8) calc(l - 0.2) h)
    ),
    var(
      --text-on-bg-primary-dark,
      oklch(from var(--palette-primary) calc(l + 0.8) calc(l - 0.2) h)
    )
  );
}

Například pro outlined tlačítko by to pak fungovalo nějak takhle:

border-primary
text-primary
hover:border-fill-hover
hover:bg-fill-hover
hover:text-text-on-fill-hover
active:border-fill-active
active:bg-fill-active
active:text-text-on-fill-active

Ano, bohužel někdy to tam bude repetitivní jako text-text, což ale bohužel v Tailwindu jinak nejde, když je třeba odlišit dvě barvy pro různé elementy (např. border-primary je jiná barva než bg-primary apod.).

Jinak záměrně do toho, zatím, nepasuji další vrstvu, která by šla už na úroveň konkrétních komponent. Pokud zjistíme, že to potřebujeme, tak se to může přidat později. Pak by tedy vzniklo i něco jako --color-button-bg-primary-hover apod.

Co si od tohoto řešení slibuju:

  • bude to velmi jednoduše themable pouhou definicí dvou, tří barev, s čímž si poradí i ne-designer
  • bude to konzistentní napříč komponentami, protože ty budou využívat výhradně sémantické design tokeny a nebude třeba přemýšlet, jako barvu má mít zrovna border input na focus apod.
  • budeme konečně dark-mode ready

Berte to jako první kick-off, ale doufám, že si to sedne a bude to fungovat. Těším se na feedback.

1 Like

Diky, je to super. Zrovna pro nas me trochu mrzi, ze Tailwind4 zrusil color mapy pro jednotlive elementy (textColor, borderColor, backgroundColor). Takze nemuzes mit text-primary, bg-primary, border-primary v jejich odpovidajicich variantach.

Ale na druhou stranu kdyz ctu tu logiku za zjednodusovanim API, tak to dava smysl. Pro vetsinu projektu to takhle proste nefungovalo.

Tak si rikam, jestli nedava smysl jit tomu naproti a definovat rovnou ty color variables per-komponenta. Tedy tu variantu --color-button-bg-primary-hover od zacatku.

Jeste nemusime pouzivat element text, ale muzeme ho mit default (fg obecne, jako je to ve Figme). To pak muzes vyhodit z tech on-barev. misto fg-on-bg mit jenom on-bg.

@mewdev objevil, že úplně nezrušil. Sice je to nezdokumentované, ale něco (asi vše co potřebujeme) funguje, např. --text-color-primary, viz playground. Je tedy otázka jestli do toho jít a trochu si to s tím zpřehlednit i za cenu toho, že se to časem může rozbít.

Jinak zatím bych do těch proměnných per komponenta nešel, dělal bych to case by case až když se ukáže, že to je u nějaké komponenty třeba, dělat to u všech najednou mi přijde jako overkill a dost zásadně se to tím znepřehlední.

Nepoužívat element text a mít jako default myslíš tak, že by prostě barva bez prefixu byla myšlena jen pro text? Do toho bych asi nešel, to může dělat neplechu, navíc můžeme dojít i k tomu, že třeba pro body a display text budeš chtít mít jinou barvu. U tohoto bych se držel toho, aby vždy byl explicitně použit ten element, na který se barva aplikuje.

Odpověď developera, který na tailwindu pracuje:

Those mainly existed for backwards compatibility, but we are also not going to take them away any time soon if at all. That said, we even have an open PR to start documenting those usages, so you can just use them.

V PR je i playground obdobný tomu našemu. Použití je stejné, takže to zřejmě bude validní approach.

Jinak zatím bych do těch proměnných per komponenta nešel, dělal bych to case by case až když se ukáže, že to je u nějaké komponenty třeba, dělat to u všech najednou mi přijde jako overkill a dost zásadně se to tím znepřehlední.

Nepoužívat element text a mít jako default myslíš tak, že by prostě barva bez prefixu byla myšlena jen pro text? Do toho bych asi nešel, to může dělat neplechu, navíc můžeme dojít i k tomu, že třeba pro body a display text budeš chtít mít jinou barvu. U tohoto bych se držel toho, aby vždy byl explicitně použit ten element, na který se barva aplikuje.

Za mě to taky je moc overkill, spíše bych to teď nechal minimální.

1 Like

Super vyzkum, pouzijme ty specificke promenne, i kdyz nejsou zdokumentovane. Me to prijde jako velke zjednoduseni a presne tak je ta nase Figma nadefinovana.

1 Like