@gravity-ui/uikit
Version:
Gravity UI base styling and components
291 lines (218 loc) • 12.1 kB
Markdown
# Theming, Colors & Branding
[English](./theming.md) | [Русский](./theming-ru.md)
We use and recommend CSS variables for theming.
Everything is driven by CSS custom properties prefixed with `--g-*`. There is no runtime styling API you need to call — you set variables and components pick them up.
```tsx
import {ThemeProvider, Button} from '@gravity-ui/uikit';
import '@gravity-ui/uikit/styles/fonts.css';
import '@gravity-ui/uikit/styles/styles.css';
export const App = () => (
<ThemeProvider theme="system">
<Button view="action">Branded button</Button>
</ThemeProvider>
);
```
## How theming works
All variables live on the root class `.g-root`, which `ThemeProvider` assigns to `<body>` by
default (change the target with the `scoped` prop). Color variables additionally live on a
per-theme class `.g-root_theme_{themeName}`, so switching the theme swaps one set of values for
another.
```
.g-root → structural tokens (spacing, typography metrics, border radius)
.g-root_theme_light → color tokens for the light theme
.g-root_theme_dark → color tokens for the dark theme
```
To customize anything, you provide new values for these CSS variables — for a single theme, for
several themes, or globally. If you support more than one theme, set color overrides **per theme**.
## Themes
UIKit ships 4 built-in themes:
| Theme | Description |
| ---------- | ------------------------------------ |
| `light` | Default light theme |
| `dark` | Default dark theme |
| `light-hc` | Light, high-contrast (accessibility) |
| `dark-hc` | Dark, high-contrast (accessibility) |
Select the theme via `ThemeProvider`:
```tsx
<ThemeProvider theme="dark">{...}</ThemeProvider>
```
The default is `"system"`, which follows the OS color-scheme preference and resolves to `light`
or `dark`. You can control what `system` resolves to with `systemLightTheme` / `systemDarkTheme`.
Read or switch the theme at runtime with the `useTheme` / `useThemeValue` hooks.
## Color token layers
Colors are organized in **two layers**. Components and app code should only ever reference the
**semantic** layer.
### Private tokens
`--g-color-private-*` are the raw palette — the actual RGB values, organized by hue and by a
numeric scale. They exist so the semantic layer has something to point at. **Do not use them
directly in application code**; they are an implementation detail and can change.
Hue families: `black`, `white`, `blue`, `green`, `yellow`, `orange`, `red`, `purple`,
`cool-grey`.
Two flavors per step:
- `--g-color-private-black-50` — translucent (`rgba(0, 0, 0, 0.05)`), blends with what's behind it.
- `--g-color-private-black-50-solid` — opaque (`rgb(242, 242, 242)`), the pre-flattened equivalent.
Use `-solid` variants when a translucent color would let an underlying element bleed through
(e.g. overlapping elements, shadows).
### Semantic tokens
`--g-color-{group}-{role}` describe _intent_, not a specific hue. This is the layer you consume.
| Group | Purpose | Examples |
| ------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------ |
| `--g-color-base-*` | Backgrounds & fills | `base-background`, `base-brand`, `base-generic`, `base-danger-medium` |
| `--g-color-text-*` | Text colors | `text-primary`, `text-secondary`, `text-hint`, `text-brand`, `text-link` |
| `--g-color-line-*` | Borders, dividers, underlines | `line-generic`, `line-brand`, `line-focus`, `line-danger` |
| `--g-color-sfx-*` | Effects — shadows, veils, fades | `sfx-shadow`, `sfx-veil`, `sfx-fade` |
| `--g-color-infographics-*` / `--g-color-scroll-*` | Charts, scrollbars | `infographics-axis`, `scroll-handle` |
**Meaning conventions inside a group:**
- **Statuses** — `info` (blue), `positive` (green), `warning` (yellow), `danger` (red),
`utility` (purple), `misc` (cool grey), plus `brand`, `generic` and `neutral`.
- **Intensity** — `light` → `medium` → `heavy` go from subtle background fills to strong,
filled surfaces. `heavy` fills are meant to carry contrasting (`*-contrast`) text on top.
- **Interaction** — a `-hover` suffix is the hover counterpart of the base token
(e.g. `base-brand` / `base-brand-hover`).
- **Text hierarchy** — `text-primary` > `text-secondary` > `text-hint` (decreasing emphasis).
```css
/* status background + matching text */
.alert-danger {
background: var(--g-color-base-danger-light);
color: var(--g-color-text-danger);
}
```
## Branding
Branding is a subset of theming: override a small, curated set of variables to make UIKit look
like your product. In most cases you only need the accent color, fonts, and border radius.
### Accent / brand color
The accent color is what makes an app feel branded — action buttons, active controls, links, and
selection highlights. Override this group (per theme):
| Variable | Used for |
| ----------------------------------- | ------------------------------------------------- |
| `--g-color-base-brand` | Brand background (action button, active controls) |
| `--g-color-base-brand-hover` | Hover background |
| `--g-color-base-selection` | Lighter brand tint (List/Table row selection) |
| `--g-color-base-selection-hover` | Hover of the selection tint |
| `--g-color-line-brand` | Brand lines (active tab underline) |
| `--g-color-text-brand` | Brand text |
| `--g-color-text-brand-heavy` | Brand text over a background |
| `--g-color-text-brand-contrast` | Text placed **on top of** a brand background |
| `--g-color-text-link` | Links |
| `--g-color-text-link-hover` | Hover of links |
| `--g-color-text-link-visited` | Visited links |
| `--g-color-text-link-visited-hover` | Hover of visited links |
```css
.g-root {
--g-color-base-brand: rgb(117, 155, 255);
--g-color-base-brand-hover: rgb(99, 143, 255);
--g-color-base-selection: rgba(82, 130, 255, 0.05);
--g-color-base-selection-hover: rgba(82, 130, 255, 0.1);
--g-color-line-brand: rgb(117, 155, 255);
--g-color-text-brand: rgb(117, 155, 255);
--g-color-text-brand-contrast: rgb(255, 255, 255);
--g-color-text-link: rgb(117, 155, 255);
--g-color-text-link-hover: rgb(82, 130, 255);
}
```
> Set these on the theme class (e.g. `.g-root_theme_light`) if the brand color should differ
> between light and dark; use `.g-root` for values shared by all themes.
### Typography
Configure fonts, weights, and per-variant metrics via `--g-font-family-*` and `--g-text-*`
variables on the root class. Full reference — variants, sizing tokens, and customization — lives
in the [**typography guide**](./typography.md).
```css
.g-root {
--g-font-family-sans: 'Inter', sans-serif;
--g-text-header-font-weight: 600;
}
```
### Shape (border radius)
Controls share a border-radius scale: `--g-border-radius-{size}` where size is one of
`xs`, `s`, `m`, `l`, `xl`. Use a token, never a hard-coded `px` value.
```css
/* your own component, in Gravity style */
.my-card {
border-radius: var(--g-border-radius-m);
}
```
Individual components expose their own radius variable aligned to the same scale, so you can
tune one component without touching the global scale — e.g. `--g-button-border-radius`,
`--g-card-border-radius`, `--g-modal-border-radius`, `--g-popup-border-radius`,
`--g-text-input-border-radius`, `--g-list-container-border-radius`, `--g-focus-border-radius`.
```css
.g-root {
--g-border-radius-m: 8px; /* whole scale step */
--g-button-border-radius: var(--g-border-radius-l); /* just buttons */
}
```
## Using colors in your code
Consume semantic tokens directly in CSS. In JS/TSX, prefer the `Text` component's `color` prop
and component `view`/`theme` props over inline colors.
```css
.card {
background: var(--g-color-base-generic);
color: var(--g-color-text-primary);
border: 1px solid var(--g-color-line-generic);
border-radius: var(--g-border-radius-l);
}
```
```tsx
import {Text} from '@gravity-ui/uikit';
<Text color="secondary">Muted caption</Text>;
```
Because these are theme-aware tokens, the same markup renders correctly in every theme and
respects any brand overrides — no conditional theme logic in your components.
## Creating a custom theme
Define a theme from scratch, or extend one of the built-ins with the SCSS mixins:
```scss
@use '@gravity-ui/uikit/styles/themes';
// Start from the light theme, then override
.g-root_theme_custom {
@include themes.g-theme-light;
// your overrides
--g-color-base-brand: rgb(117, 155, 255);
}
```
Available mixins: `themes.g-theme-light`, `themes.g-theme-dark`, `themes.g-theme-light-hc`,
`themes.g-theme-dark-hc`. Then pass your theme name to the provider:
```tsx
<ThemeProvider theme="custom">{...}</ThemeProvider>
```
### Rebranding: do it completely
> **Don't override just 2–4 tokens.** If you only set `--g-color-base-brand`, then selection,
> focus, links and `*-contrast` colors stay on the default accent and your UI ends up
> mismatched. Override the **full brand token set** (the [accent table](#accent--brand-color)
> above) — and provide values for **each theme** you support (dark themes usually need a
> brighter brand color than light).
Practical ways to produce a complete, consistent token set:
- **[Themer web tool](https://gravity-ui.com/themer)** — pick a couple of brand colors in the
browser and export a ready theme as CSS or JSON.
- **[`@gravity-ui/uikit-themer`](https://github.com/gravity-ui/uikit-themer)** — the same
generator as a library, for producing themes programmatically or wiring them into a build step.
```shell
npm install @gravity-ui/uikit-themer
```
```ts
import {generateCSS, updateBaseColor, DEFAULT_THEME} from '@gravity-ui/uikit-themer';
// Change a base color; private/dependent tokens are recalculated for you.
const theme = updateBaseColor({
theme: DEFAULT_THEME,
colorToken: 'brand',
value: {light: '#007AFF', dark: '#007AFF'},
});
// Emit CSS with .g-root_theme_light / .g-root_theme_dark blocks.
const css = generateCSS({theme, ignoreDefaultValues: true});
```
It also exposes `generateJSON` / `parseCSS` / `parseJSON` and CSS↔JSON converters. Always use
`updateBaseColor` (rather than editing tokens by hand) so the private palette regenerates and
the theme stays internally consistent.
- **SCSS mixins** — extend a built-in theme (above) and layer overrides on top.
Whichever you use, **import the generated theme file _after_ `styles.css`** so it wins the
cascade; `ThemeProvider` activates it via the theme class automatically. Keep the brand
definition in a single theme file — don't search-and-replace `--g-*` variables across your codebase.
### Scoped themes
To apply a different theme to just one region (e.g. a dark toolbar in a light app), nest a
`scoped` provider — it sets the theme class locally and updates React context for descendants
without polluting the global root:
```tsx
<ThemeProvider scoped theme="dark">
<Toolbar />
</ThemeProvider>
```
For a CSS-only region (no context update), apply the class from `getRootClassName({theme: 'dark'})`.