Theming, Colors & Branding

September 1, 2026 · View on GitHub

English | Русский

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:

ThemeDescription
lightDefault light theme
darkDefault dark theme
light-hcLight, high-contrast (accessibility)
dark-hcDark, 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.

GroupPurposeExamples
--g-color-base-*Backgrounds & fillsbase-background, base-brand, base-generic, base-danger-medium
--g-color-text-*Text colorstext-primary, text-secondary, text-hint, text-brand, text-link
--g-color-line-*Borders, dividers, underlinesline-generic, line-brand, line-focus, line-danger
--g-color-sfx-*Effects — shadows, veils, fadessfx-shadow, sfx-veil, sfx-fade
--g-color-infographics-* / --g-color-scroll-*Charts, scrollbarsinfographics-axis, scroll-handle

Meaning conventions inside a group:

  • Statusesinfo (blue), positive (green), warning (yellow), danger (red), utility (purple), misc (cool grey), plus brand, generic and neutral.
  • Intensitylightmediumheavy 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 hierarchytext-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):

VariableUsed for
--g-color-base-brandBrand background (action button, active controls)
--g-color-base-brand-hoverHover background
--g-color-base-selectionLighter brand tint (List/Table row selection)
--g-color-base-selection-hoverHover of the selection tint
--g-color-line-brandBrand lines (active tab underline)
--g-color-text-brandBrand text
--g-color-text-brand-heavyBrand text over a background
--g-color-text-brand-contrastText placed on top of a brand background
--g-color-text-linkLinks
--g-color-text-link-hoverHover of links
--g-color-text-link-visitedVisited links
--g-color-text-link-visited-hoverHover 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-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.

.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 *-contrast colors 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-themer
    
    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:

<ThemeProvider scoped theme="dark">
  <Toolbar />
</ThemeProvider>

For a CSS-only region (no context update), apply the class from getRootClassName({theme: 'dark'}).