Theming, Colors & Branding
September 1, 2026 · View on GitHub
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.
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:
<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.
Default component props
Use ThemeProvider.defaultProps to set application-wide defaults for UIKit components:
import type {ComponentDefaultPropsMap} from '@gravity-ui/uikit';
import {Button, ThemeProvider} from '@gravity-ui/uikit';
const defaultProps = {
Button: {size: 'l', view: 'outlined'},
} satisfies ComponentDefaultPropsMap;
<ThemeProvider defaultProps={defaultProps}>
<Button>Large outlined button</Button>
</ThemeProvider>;
Use DefaultPropsProvider to override defaults for a subtree without creating another theme
scope:
import type {ComponentDefaultPropsMap} from '@gravity-ui/uikit';
import {Button, DefaultPropsProvider} from '@gravity-ui/uikit';
const actionButtonDefaults = {
Button: {view: 'action'},
} satisfies ComponentDefaultPropsMap;
<DefaultPropsProvider defaultProps={actionButtonDefaults}>
<Button>Action button</Button>
</DefaultPropsProvider>;
Explicit component props have the highest priority. A prop set to undefined does not override a
default. Nested providers inherit entries for other components, but replace the complete defaults
object for the same component. For example, an inner Button: {view: 'action'} replaces both the
view and size from an outer Button: {view: 'outlined', size: 'l'}. Resetting all inherited
defaults for a subtree is not currently supported.
Keep the defaultProps object referentially stable by defining it outside render or wrapping it in
React.useMemo. Passing an inline object creates a new context value on every parent render and
causes components that consume defaults to update.
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), plusbrand,genericandneutral. - Intensity —
light→medium→heavygo from subtle background fills to strong, filled surfaces.heavyfills are meant to carry contrasting (*-contrast) text on top. - Interaction — a
-hoversuffix 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).
/* 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 |
.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-rootfor 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.
.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.
/* 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.
.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.
.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);
}
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:
@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:
<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*-contrastcolors stay on the default accent and your UI ends up mismatched. Override the full brand token set (the accent table 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 — pick a couple of brand colors in the browser and export a ready theme as CSS or JSON.
-
@gravity-ui/uikit-themer— the same generator as a library, for producing themes programmatically or wiring them into a build step.npm install @gravity-ui/uikit-themerimport {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/parseJSONand CSS↔JSON converters. Always useupdateBaseColor(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:
<ThemeProvider scoped theme="dark">
<Toolbar />
</ThemeProvider>
For a CSS-only region (no context update), apply the class from getRootClassName({theme: 'dark'}).