Colors
Grundtone uses 38 semantic color tokens defined in @grundtone/core. Every token maps to a CSS custom property (--color-{kebab-case}). Core is the single source of truth — design-tokens and Vue/Nuxt derive their values from it.
Naming Convention
Tokens use shade-based naming (primaryLight, primaryDark) instead of state-based (primaryHover, primaryActive). Components decide which shade to use for hover, active, tint, etc. This is more flexible and consistent across the system.
Brand
| Color | Token | CSS Variable | Purpose |
|---|---|---|---|
#0059b3 | primary | --color-primary | Primary brand color |
#3381cc | primaryLight | --color-primary-light | Lighter shade (hover, tints) |
#003a7a | primaryDark | --color-primary-dark | Darker shade (active, pressed) |
#ffffff | onPrimary | --color-on-primary | Text on primary background |
#6c757d | secondary | --color-secondary | Secondary brand color |
#868e96 | secondaryLight | --color-secondary-light | Lighter secondary shade |
#494f54 | secondaryDark | --color-secondary-dark | Darker secondary shade |
Status
| Color | Token | CSS Variable | Purpose |
|---|---|---|---|
#198754 | success | --color-success | Success state |
#d1e7dd | successLight | --color-success-light | Success background tint |
#146c43 | successDark | --color-success-dark | Darker success shade |
#ffc107 | warning | --color-warning | Warning state |
#fff3cd | warningLight | --color-warning-light | Warning background tint |
#cc9a06 | warningDark | --color-warning-dark | Darker warning shade |
#dc3545 | error | --color-error | Error state |
#f8d7da | errorLight | --color-error-light | Error background tint |
#b02a37 | errorDark | --color-error-dark | Darker error shade |
#0dcaf0 | info | --color-info | Info state |
#cff4fc | infoLight | --color-info-light | Info background tint |
#0aa2c0 | infoDark | --color-info-dark | Darker info shade |
Surface
| Color | Token | CSS Variable | Purpose |
|---|---|---|---|
#ffffff | background | --color-background | Page background |
#fafafa | backgroundAlt | --color-background-alt | Alternate sections, zebra rows |
#f8f9fa | surface | --color-surface | Cards, panels |
#f0f1f2 | surfaceAlt | --color-surface-alt | Hover state, zebra |
#ffffff | surfaceRaised | --color-surface-raised | Modals, FABs |
rgba(255,255,255,0.95) | surfaceOverlay | --color-surface-overlay | Semi-transparent overlay |
rgba(0,0,0,0.5) | modalBackdrop | --color-modal-backdrop | Modal backdrop scrim |
Background Utilities
Shorthand classes for applying surface/background tokens:
| Class | CSS Variable |
|---|---|
.bg-normal | var(--color-background) |
.bg-alt | var(--color-background-alt) |
.bg-surface | var(--color-surface) |
.bg-surface-alt | var(--color-surface-alt) |
.bg-surface-raised | var(--color-surface-raised) |
.bg-overlay | var(--color-surface-overlay) |
.bg-modal | var(--color-modal-backdrop) |
<!-- Modal backdrop -->
<div class="fixed inset-0 z-modal-backdrop bg-modal">
<div class="z-modal">...</div>
</div>Text
| Color | Token | CSS Variable | Purpose |
|---|---|---|---|
#212529 | text | --color-text | Primary text |
#6c757d | textSecondary | --color-text-secondary | Secondary text |
#adb5bd | textTertiary | --color-text-tertiary | Tertiary / hint text |
#ffffff | textInverse | --color-text-inverse | Text on dark backgrounds |
#a3a3a3 | textPlaceholder | --color-text-placeholder | Input placeholders |
#d4d4d4 | textDisabled | --color-text-disabled | Disabled elements |
Border
| Color | Token | CSS Variable | Purpose |
|---|---|---|---|
#dee2e6 | borderLight | --color-border-light | Subtle dividers |
#ced4da | borderMedium | --color-border-medium | Default inputs |
#adb5bd | borderStrong | --color-border-strong | Focus, emphasized |
rgba(255,255,255,0.2) | borderInverse | --color-border-inverse | On dark backgrounds |
Focus & Neutral
| Color | Token | CSS Variable | Purpose |
|---|---|---|---|
#0059b3 | focus | --color-focus | Focus indicator |
rgba(0,89,179,0.25) | focusRing | --color-focus-ring | Focus ring shadow |
#6c757d | neutral | --color-neutral | Neutral UI elements |
Light vs Dark Comparison
Side-by-side comparison of all tokens across modes.
Brand
| Color | Token | Light | Dark |
|---|---|---|---|
#0059b3 | primary | #0059b3 | #4dabf7 |
#3381cc | primaryLight | #3381cc | #74c0fc |
#003a7a | primaryDark | #003a7a | #339af0 |
#ffffff | onPrimary | #ffffff | #121212 |
#6c757d | secondary | #6c757d | #adb5bd |
#868e96 | secondaryLight | #868e96 | #ced4da |
#494f54 | secondaryDark | #494f54 | #868e96 |
Status
| Color | Token | Light | Dark |
|---|---|---|---|
#198754 | success | #198754 | #51cf66 |
#d1e7dd | successLight | #d1e7dd | #1a3d20 |
#146c43 | successDark | #146c43 | #40c057 |
#ffc107 | warning | #ffc107 | #ffd43b |
#fff3cd | warningLight | #fff3cd | #3d3a1a |
#cc9a06 | warningDark | #cc9a06 | #fab005 |
#dc3545 | error | #dc3545 | #ff6b6b |
#f8d7da | errorLight | #f8d7da | #3d1a1c |
#b02a37 | errorDark | #b02a37 | #fa5252 |
#0dcaf0 | info | #0dcaf0 | #4dabf7 |
#cff4fc | infoLight | #cff4fc | #1a2e3d |
#0aa2c0 | infoDark | #0aa2c0 | #339af0 |
Surface
| Color | Token | Light | Dark |
|---|---|---|---|
#ffffff | background | #ffffff | #121212 |
#fafafa | backgroundAlt | #fafafa | #1a1a1a |
#f8f9fa | surface | #f8f9fa | #1e1e1e |
#f0f1f2 | surfaceAlt | #f0f1f2 | #252525 |
#ffffff | surfaceRaised | #ffffff | #2a2a2a |
rgba(255,255,255,0.95) | surfaceOverlay | rgba(255,255,255,0.95) | rgba(30,30,30,0.95) |
rgba(0,0,0,0.5) | modalBackdrop | rgba(0,0,0,0.5) | rgba(0,0,0,0.7) |
Text
| Color | Token | Light | Dark |
|---|---|---|---|
#212529 | text | #212529 | #ffffff |
#6c757d | textSecondary | #6c757d | #b0b0b0 |
#adb5bd | textTertiary | #adb5bd | #808080 |
#ffffff | textInverse | #ffffff | #121212 |
#a3a3a3 | textPlaceholder | #a3a3a3 | #666666 |
#d4d4d4 | textDisabled | #d4d4d4 | #4a4a4a |
Border
| Color | Token | Light | Dark |
|---|---|---|---|
#dee2e6 | borderLight | #dee2e6 | #404040 |
#ced4da | borderMedium | #ced4da | #505050 |
#adb5bd | borderStrong | #adb5bd | #606060 |
rgba(255,255,255,0.2) | borderInverse | rgba(255,255,255,0.2) | rgba(0,0,0,0.3) |
Usage
CSS
.card {
background: var(--color-surface);
border: 1px solid var(--color-border-light);
color: var(--color-text);
}
.card:hover {
background: var(--color-surface-alt);
border-color: var(--color-border-strong);
}SCSS — Override Colors
Override any semantic token at build time via @use ... with ():
@use '@grundtone/design-tokens/scss' with (
$color-overrides-light: (
'primary': #e91e63,
'primary-light': #f06292,
'primary-dark': #c2185b,
),
$color-overrides-dark: (
'primary': #f48fb1,
)
);Or import colors standalone:
@use '@grundtone/design-tokens/scss/colors' with (
$overrides-light: (
'primary': #e91e63,
),
$overrides-dark: (
'primary': #f48fb1,
)
);SCSS — Raw Palette
For compile-time access to raw palette colors (not CSS custom properties), import the palette module. This is useful for generating static styles, calculating contrast, or building component variants.
@use '@grundtone/design-tokens/scss/color-palette' as palette;palette.color($family, $shade)
Get a specific shade from a color family.
border-color: palette.color('blue', 300); // #93c5fd
background: palette.color('gray', 50); // #fafafa
color: palette.color('red', 700); // #b91c1cAvailable families: gray, blue, green, red, yellow, indigo. Available shades: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900.
palette.lighter($family, $shade, $steps)
Move up (lighter) in the shade scale by N steps. Clamps at 50.
$base: palette.color('blue', 500); // #3b82f6
$light: palette.lighter('blue', 500); // #60a5fa (one step = 400)
$lighter: palette.lighter('blue', 500, 3); // #bfdbfe (three steps = 200)palette.darker($family, $shade, $steps)
Move down (darker) in the shade scale by N steps. Clamps at 900.
$base: palette.color('blue', 500); // #3b82f6
$dark: palette.darker('blue', 500); // #2563eb (one step = 600)
$darker: palette.darker('blue', 500, 3); // #1e40af (three steps = 800)palette.alpha($family, $shade, $alpha)
Create a semi-transparent variant of a palette color.
background: palette.alpha('blue', 500, 0.1); // rgba(59, 130, 246, 0.1)
box-shadow: 0 4px 12px palette.alpha('gray', 900, 0.15);
border-color: palette.alpha('red', 500, 0.3);When to use palette vs CSS custom properties
Use CSS custom properties (var(--color-primary)) for all runtime styles — they respect theming and dark mode. Use palette functions only when you need a static color value at compile time, such as SCSS calculations, contrast checks, or generating component variants that don't change with theme.
React Native
import { useGrundtoneTheme } from '@grundtone/react-native';
function Card() {
const { theme } = useGrundtoneTheme();
return (
<View
style={{
backgroundColor: theme.colors.surface,
borderColor: theme.colors.borderLight,
}}
>
<Text style={{ color: theme.colors.text }}>Content</Text>
</View>
);
}Migration from Old Names
If you are upgrading from a previous version, rename these tokens:
| Old Name | New Name |
|---|---|
primaryHover | primaryLight |
primaryActive | primaryDark |
secondaryHover | secondaryLight |
secondaryActive | secondaryDark |
successBg | successLight |
warningBg | warningLight |
errorBg | errorLight |
infoBg | infoLight |
surfaceHover | surfaceAlt |
border | borderLight |
borderHover | borderStrong |
CSS variable equivalents:
| Old CSS Variable | New CSS Variable |
|---|---|
--color-primary-hover | --color-primary-light |
--color-primary-active | --color-primary-dark |
--color-secondary-hover | --color-secondary-light |
--color-secondary-active | --color-secondary-dark |
--color-success-bg | --color-success-light |
--color-warning-bg | --color-warning-light |
--color-error-bg | --color-error-light |
--color-info-bg | --color-info-light |
--color-surface-hover | --color-surface-alt |
--color-border | --color-border-light |
--color-border-hover | --color-border-strong |
Theming
See Theme Configuration for the concept overview and createTheme() API. This section covers web-specific setup for each package.
CSS Variables (Web)
On web, the theme is applied as CSS custom properties on :root:
--color-primary
--color-primary-light
--color-primary-dark
--color-background
--color-text
/* etc. — 38 tokens total */Use them in your styles:
.button {
background-color: var(--color-primary);
color: var(--color-on-primary);
}Vue 3
Where: ThemeProvider in App.vue
How: Pass createTheme() to the theme prop. Override only the colors you need — the rest uses defaults.
<template>
<ThemeProvider :theme="myTheme" mode="auto">
<RouterView />
</ThemeProvider>
</template>
<script setup lang="ts">
import { ThemeProvider } from '@grundtone/vue';
import { createTheme } from '@grundtone/core';
const myTheme = createTheme({
light: {
primary: '#0059b3',
primaryLight: '#3381cc',
primaryDark: '#003a7a',
onPrimary: '#ffffff',
},
dark: {
primary: '#4dabf7',
primaryLight: '#74c0fc',
primaryDark: '#339af0',
onPrimary: '#121212',
},
});
</script>Alternative: single partial theme (same overrides for both modes):
<template>
<ThemeProvider :theme="myTheme" mode="auto">
<App />
</ThemeProvider>
</template>
<script setup lang="ts">
import { ThemeProvider } from '@grundtone/vue';
const myTheme = {
colors: {
primary: '#e91e63',
primaryLight: '#f06292',
primaryDark: '#c2185b',
},
};
</script>Use ThemeToggle from @grundtone/vue or useTheme().toggleMode() to let users switch themes.
Nuxt 3
Where: nuxt.config.ts
How: Add theme to the grundtone module config. Use createTheme() for separate light and dark themes.
// nuxt.config.ts
import { createTheme } from '@grundtone/core';
export default defineNuxtConfig({
modules: ['@grundtone/nuxt'],
grundtone: {
theme: createTheme({
light: {
primary: '#0059b3',
primaryLight: '#3381cc',
primaryDark: '#003a7a',
onPrimary: '#ffffff',
},
dark: {
primary: '#4dabf7',
primaryLight: '#74c0fc',
primaryDark: '#339af0',
onPrimary: '#121212',
},
}),
},
});The Nuxt plugin applies the correct theme based on system preference (prefers-color-scheme) and reacts to changes.
Plain Web (no framework)
Where: Your own CSS or build step
How: @grundtone/design-tokens ships static :root variables. Override them in your stylesheet after importing the design-tokens CSS.
<link rel="stylesheet" href="node_modules/@grundtone/design-tokens/dist/index.css" />
<style>
:root {
--color-primary: #e91e63;
--color-primary-light: #f06292;
--color-primary-dark: #c2185b;
--color-on-primary: #ffffff;
/* Override more as needed */
}
</style>With a bundler (Vite, webpack), import and override in your main CSS/SCSS:
@import '@grundtone/design-tokens/dist/index.css';
:root {
--color-primary: #e91e63;
--color-primary-light: #ec407a;
}There is no ThemeProvider for Plain Web, so no automatic light/dark switching. For dark mode, use media queries or a class:
@media (prefers-color-scheme: dark) {
:root {
--color-primary: #4dabf7;
--color-background: #121212;
}
}
/* Or with a class */
[data-theme='dark'] {
--color-primary: #4dabf7;
--color-background: #121212;
}Custom Colors (SCSS)
Colors use the same @use ... with () override pattern as breakpoints. Default values live in _color-defaults.scss (derived from @grundtone/core). Override only what you need — the rest keeps its defaults via map.merge.
Override via the main entry point:
@use '@grundtone/design-tokens/scss' with (
$color-overrides-light: (
'primary': #e91e63,
'primary-light': #f06292,
'primary-dark': #c2185b,
),
$color-overrides-dark: (
'primary': #f48fb1,
'primary-light': #f8bbd0,
'primary-dark': #ec407a,
)
);Or import colors standalone (e.g. if you only need color tokens, not the full design system):
@use '@grundtone/design-tokens/scss/colors' with (
$overrides-light: (
'primary': #e91e63,
'primary-light': #f06292,
'primary-dark': #c2185b,
),
$overrides-dark: (
'primary': #f48fb1,
)
);Token keys use kebab-case matching the CSS variable names (e.g. 'primary-light' for --color-primary-light). See the token tables above for all available tokens.
Colors and breakpoints can be overridden together in a single
@usestatement.
Custom Breakpoints
Breakpoints are compiled into @media queries at build time via SCSS. The default values live in a single source of truth (_breakpoints-defaults.scss). Every file — grid utilities, container max-widths, CSS custom properties, and @property declarations — derives its values from this one map.
Override $grid-breakpoints before importing design-tokens:
@use '@grundtone/design-tokens/scss' with (
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px,
2xl: 1400px,
)
);Or when importing just breakpoints:
@use '@grundtone/design-tokens/scss/breakpoints' as bp with (
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px,
2xl: 1400px,
)
);Defaults: sm 640px, md 768px, lg 1024px, xl 1280px, 2xl 1536px.
The CSS custom properties
--breakpoint-smetc. are exported to:rootfor informational use (e.g. in JavaScript) but do not affect the compiled media queries. When you override$grid-breakpoints, the:rootCSS vars update automatically.
See Breakpoints for media query mixins and utility class prefixes.
