@gainsight-hub/design-tokens
Design tokens for the Gainsight Community Hub — CSS custom properties, TypeScript constants, JSON, and a Tailwind v4 theme.
Install
npm install @gainsight-hub/design-tokens
This package has no runtime dependencies and works on its own: plain CSS apps and SCSS apps
(for example a server-rendered Preact destination) install only this package and import
styles.css and a theme. tailwindcss (>= 3) is an optional peer, needed only for the Tailwind
preset. Install @gainsight-hub/design-system or @gainsight-hub/theme-editor only if you need
React components or the theme editor UI; both peer-depend on this package, so the app that owns the
tokens also owns their version.
Quick start
Import styles.css once at your app entry point. That's it for most apps.
// main.js / main.ts
import "@gainsight-hub/design-tokens/styles.css";
Then use tokens anywhere via CSS custom properties:
.button {
background: var(--color-action-primary-default);
color: var(--color-content-inverse);
border-radius: var(--button-radius);
}
CSS custom properties
Base (light theme)
Defines all tokens as CSS custom properties on :root. This is the default light theme.
import "@gainsight-hub/design-tokens/styles.css";
All themes bundled
Includes the base tokens plus all theme overrides in a single file. Use this if you want to switch themes at runtime without loading additional stylesheets.
import "@gainsight-hub/design-tokens/themes.css";
Individual themes
Load only the theme you need on top of styles.css:
import "@gainsight-hub/design-tokens/styles.css";
import "@gainsight-hub/design-tokens/themes/ocean.css";
Available themes — each ships both a light block ([data-theme="{name}"]) and a
dark block ([data-theme="{name}"].dark), except legacy, which is light-only
(it's the pre-migration compat bridge — see below):
| Import path | Description |
|---|---|
@gainsight-hub/design-tokens/themes/ocean.css | Ocean — blue palette |
@gainsight-hub/design-tokens/themes/ember.css | Ember — warm orange palette |
@gainsight-hub/design-tokens/themes/meadow.css | Meadow — green palette |
@gainsight-hub/design-tokens/themes/violet.css | Violet — indigo palette |
@gainsight-hub/design-tokens/themes/slate.css | Slate — cool-gray palette |
@gainsight-hub/design-tokens/themes/legacy.css | Pre-migration tenant customization bridge (light-only) |
The base theme's own dark mode needs no extra import — it's already in
styles.css, as a :where(:root:not([data-theme])).dark block alongside the light :root defaults (:where() keeps it at the same specificity as the light block, so runtime overrides still win).
Theme switching at runtime
data-theme always holds the selected theme's id (or is absent for the
default theme); .dark is an independent, additive class for dark mode —
never a value data-theme itself takes. Both live on document.documentElement.
/* A named theme, dark mode: */
/* (written :where([data-theme="ocean"]).dark in the generated files) */
[data-theme="ocean"].dark { /* ... */ }
/* The default theme, dark mode (no data-theme attribute): */
:root.dark { /* ... (generated as :where(:root:not([data-theme])).dark) */ }
/* Scope to a tenant / white-label */
[data-tenant="acme"] {
--color-action-primary-default: #e63946;
--button-primary-background-color-default: #e63946;
}
Token naming
Names are the public contract: a rename is a breaking change for theme overrides and customer styles.
| Layer | Pattern | Example |
|---|---|---|
| Theme inputs | --theme-{scale}-{step} | --theme-brand-700 |
| Colour primitives | --color-{palette}-{step} | --color-brand-700, --color-neutral-a100 |
| Semantic colours | --color-{role}-{purpose}[-{state}] | --color-action-primary-hover |
| Scales | --{category}-{step} | --space-200, --radius-100, --font-size-300 |
| Type styles | --text-{style}-{font-size|font-weight|line-height} | --text-heading-small-font-size |
| Component tokens | --{component}-{variant}-{property}-{state} | --button-primary-background-color-hover |
| Component geometry | --{component}-{property} | --button-radius, --sidebar-radius |
Rules for component tokens:
- Name the CSS property, state last. Colours say which colour:
background-color,border-color,content-color(text and icons),title-color,subtitle-color,icon-color, andborder-top-color/border-bottom-colorfor single sides.--button-primary-background-color-hover,--sidebar-border-color-default. The state (default,hover,pressed) is always the last word. - Why
-color: a barebackgroundorbordercan be a colour, an image or a width.--widget-hero-background-imageis an image,--sidebar-border-widthis a width,--sidebar-border-color-defaultis a colour. - Other properties keep their own word:
border-width,radius,shadow,font-family,font-weight,text-transform,height. A property with a single value takes no state suffix (--sidebar-radius). - A variant is part of the name, not a state:
--button-vote-selected-background-color-defaultis the "voted" variant of the vote button, with its own default, hover and pressed states. - Namespaces:
sidebaris a content-area widget,sidenavis the application-shell side navigation, andnavigationis the top bar and its dropdowns.
Scales worth knowing:
- Alpha:
--color-neutral-a100toa500are neutral 1000 at 4, 8, 16, 32 and 48% opacity. - Line height:
--line-height-ratio-*is a unitless multiplier (headings use tight, body and code use relaxed);--line-height-grid-*is a fixed rem value on a 4px grid (labels, caption, menu, navigation, button). --font-size-baseis in px on purpose. It is the root size every rem step derives from, and the theme editor's text-size setting writes it.
Every token carries a description, in the CSS comments and in token-catalog.json.
TypeScript / JavaScript
Import typed token constants directly. All exports are fully typed via the bundled tokens.d.ts.
import {
ColorActionPrimaryDefault,
ColorSurfacePage,
RadiusFull,
} from "@gainsight-hub/design-tokens";
// Use in JS logic — e.g. canvas drawing, charting libs
ctx.fillStyle = ColorActionPrimaryDefault; // "#0C66E4"
Tailwind v4
Tailwind v4 only. Import the variables first, then the theme file, in your CSS entry:
@import "tailwindcss";
@import "@gainsight-hub/design-tokens/styles.css"; /* defines the CSS custom properties */
@import "@gainsight-hub/design-tokens/tailwind-theme.css"; /* maps them to Tailwind utilities */
tailwind-theme.css is an @theme inline block in which every entry is a var(--token) reference, so no value is duplicated. Change a token at any scope (for example with a theme) and every utility that uses it updates, with no rebuild.
<!-- Colours: the full token name, including -default -->
<div class="bg-action-primary-default text-content-inverse">Primary</div>
<div class="bg-surface-default text-content-default border border-line-default">Card</div>
<!-- Spacing, radius, elevation -->
<div class="p-space-200 gap-space-100 rounded-100 shadow-elevation-1">Layout</div>
<!-- Type styles: one class sets size, line height and weight -->
<h2 class="text-heading-small">Heading</h2>
<p class="font-sans text-body-medium">Body text</p>
| Tailwind class | CSS variable |
|---|---|
bg-action-primary-default | var(--color-action-primary-default) |
bg-surface-page | var(--color-surface-page) |
text-content-subtle | var(--color-content-subtle) |
border-line-default | var(--color-line-default) |
text-heading-small | --text-heading-small-font-size, -line-height and -font-weight |
font-sans | var(--font-family-sans) |
leading-grid-100 | var(--line-height-grid-100) |
rounded-100 | var(--radius-100) |
The stock scale utilities (rounded-md, text-sm, p-3, shadow-lg) are bound to tokens too, so a theme can change shape, density and elevation.
Component tokens (CSS Modules)
For component internals, use CSS Modules with var() directly:
/* Button.module.css */
.root {
background: var(--button-primary-background-color-default);
color: var(--button-primary-label-default);
border-radius: var(--button-radius);
padding: var(--space-100) var(--space-200);
}
.root:hover {
background: var(--button-primary-background-color-hover);
}
// Button.tsx
import styles from "./Button.module.css";
export function Button({ children }: { children: React.ReactNode }) {
return <button className={styles.root}>{children}</button>;
}
JSON
Raw token values as nested JSON. Useful for tooling, build scripts, or non-web targets (e.g. React Native, Figma plugins).
import tokens from "@gainsight-hub/design-tokens/tokens.json" with { type: "json" };
const primary = tokens.color.action.primary.default.value; // "#0C66E4"
const gutter = tokens.space.layout.gutter.value; // "12px"
Token catalog
Structured catalog with resolved values, descriptions, alias chains, and legacy mappings. Used by the design portal and design tooling.
import catalog from "@gainsight-hub/design-tokens/token-catalog.json" with { type: "json" };
// 224 tokens with cssVar, path, type, value, description, references, legacyMapping
catalog.all.forEach(token => console.log(token.cssVar, token.value));
Runtime theme switching
applyThemeConfig(config, mode) applies a saved ThemeConfig (theme id + any
per-mode overrides) to the current page — synchronous, no page reload. It sets
data-theme to the real theme id (or removes it for the default theme) and
toggles the .dark class independently for mode, then injects the theme's
own variables plus any overrides as CSS custom properties.
Requires the theme's own CSS to already be loaded (e.g. themes.css, or the
specific themes/{name}.css you need) — this function only manages
data-theme/.dark and layers config-level overrides on top; it doesn't fetch
stylesheets.
import "@gainsight-hub/design-tokens/themes.css"; // base + all themes
import { applyThemeConfig } from "@gainsight-hub/design-tokens";
// Switch to Ocean, light mode
applyThemeConfig({ themeId: "ocean", overrides: { light: {}, dark: {} } }, "light");
// Same theme, dark mode
applyThemeConfig({ themeId: "ocean", overrides: { light: {}, dark: {} } }, "dark");
// Default theme + a custom brand color override
applyThemeConfig(
{ themeId: "gainsight-light", overrides: { light: { "--theme-brand-700": "#FF6600" }, dark: {} } },
"light",
);
Available theme IDs: gainsight-light (default), ocean, ember, meadow, violet, slate, legacy.
Available exports
| Import | Description |
|---|---|
@gainsight-hub/design-tokens | applyThemeConfig + TypeScript/JS named constants |
@gainsight-hub/design-tokens/styles.css | CSS custom properties — base light theme |
@gainsight-hub/design-tokens/themes.css | CSS custom properties — all themes bundled |
@gainsight-hub/design-tokens/themes/* | Individual theme overrides (load on top of styles.css) |
@gainsight-hub/design-tokens/tailwind-theme.css | Tailwind v4 @theme inline mapping (import after styles.css) |
@gainsight-hub/design-tokens/tokens.json | Nested JSON — raw token values |
@gainsight-hub/design-tokens/token-catalog.json | Full catalog with resolved values, descriptions, alias chains |
Package structure
packages/design-tokens/
├── src/ Token source files (Style Dictionary format)
│ ├── primitive/ Raw scales — color, space, radius, font, shadow
│ ├── semantic/ Semantic roles — action, surface, content, line, status
│ └── component/ Component tokens — button, card, feed, navigation, widget
├── config/ Style Dictionary config and custom formats
│ └── themes/ Theme override definitions
├── migration/ Legacy variable mapping tools
├── build.js Build entry point
└── dist/ Published output (generated, do not edit)
├── index.js Root entry — applyThemeConfig + token constants
├── index.d.ts TypeScript declarations for root entry
├── applyTheme.js Runtime theme switcher (lazy CSS loading)
├── applyTheme.d.ts TypeScript declarations for applyThemeConfig
├── tokens.js JS named constants
├── tokens.d.ts TypeScript declarations for token constants
├── styles.css CSS custom properties (base light theme)
├── themes.css All themes combined (styles.css + all theme overrides)
├── tokens.json Nested JSON
├── token-catalog.json Full token catalog
├── tailwind-theme.css Tailwind v4 theme mapping
└── themes/ Individual theme CSS files
Building and publishing
# Build from the monorepo root
turbo run build --filter=@gainsight-hub/design-tokens
# Publish
npm publish --access public -w packages/design-tokens