Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
399c188
docs(gamut-docs): add the color system concept page
dreamwasp Oct 29, 2026
ed1f8a6
docs(gamut-docs): list the rebuilt admin, platform, percipio and lxSt…
dreamwasp Oct 29, 2026
e5165c3
Merge branch 'cass-gmt-1825-color-legacy-brands' into cass-gmt-1825-c…
dreamwasp Oct 29, 2026
41c22f7
docs(gamut-docs): navy and beige unpinned; status alerts carry their ink
dreamwasp Oct 29, 2026
fcd29d1
docs(gamut-docs): percipioTheme is removed; five brands ship
dreamwasp Nov 4, 2026
f11a9dd
Merge remote-tracking branch 'origin/cass-gmt-1825-color-legacy-brand…
dreamwasp Nov 4, 2026
fe7333e
docs(gamut-docs): color system page follows design's sandbox update
dreamwasp Nov 4, 2026
0c54a72
Merge cass-gmt-1825-color-legacy-brands into cass-gmt-1825-color-docs
dreamwasp Nov 4, 2026
bdab784
docs(gamut-docs): skillsoftTheme replaces percipioTheme; 204 componen…
dreamwasp Nov 4, 2026
1f4d7fa
Merge branch 'cass-gmt-1825-color-legacy-brands' into cass-gmt-1825-c…
dreamwasp Nov 4, 2026
2d9af71
Merge remote-tracking branch 'origin/cass-gmt-1825-color-legacy-brand…
dreamwasp Nov 4, 2026
75a06be
Merge branch 'cass-gmt-1825-color-white-label' into cass-gmt-1825-col…
dreamwasp Nov 5, 2026
ed87041
docs(gamut-docs): color system page covers per-brand CSS and building…
dreamwasp Nov 5, 2026
ab01baf
Merge branch 'cass-gmt-1825-color-white-label' into cass-gmt-1825-col…
dreamwasp Nov 5, 2026
47d1213
Merge branch 'cass-gmt-1825-color-docs' into cass-gmt-1825-color-site
dreamwasp Nov 5, 2026
5086aa2
docs(gamut-docs): move the docs site onto the color system
dreamwasp Nov 5, 2026
c30e2cb
refactor: use the singular --color- prefix for color variables
dreamwasp Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .changeset/color-system-brands.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
'@skillsoft/gamut': minor
---

Brands on the generated color system, and `--colors-*` variable names.
Brands on the generated color system.

- **Breaking:** color CSS variables are renamed from `--color-*` to `--colors-*`, in `@skillsoft/variance`'s `createTheme` and in `tokens.css`. Raw CSS that reads `var(--color-…)` has to be updated.
- **Breaking:** `tokens.css` puts its rules in `@layer tokens`, with `reset, base, tokens, recipes, utilities` declared first.
- Adds `originalCodecademyTheme`, today's core colors, fonts and hard-offset elevation rebuilt on the generated system. `tokens.css` carries a palette block per brand under `[data-brand]`.
- A brand can override a palette step with an exact hex, or point a role at a different step (and, optionally, a different alpha) in one mode. Overrides go through the same contrast checks as generated values.
Expand Down
40 changes: 20 additions & 20 deletions packages/gamut-agent-tools/skills/gamut-review/SKILL.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const Sparkline = () => {
};
```

**For colors, `theme.colors[key]` is usually enough — it works with aliases and doesn't need a second lookup step.** `theme.colors[key]` (including semantic aliases like `'primary'` or `'background-current'`) returns a live CSS variable reference (`var(--colors-key)`), which the browser resolves through the normal light/dark cascade. This works as a plain attribute value on DOM-rendered elements — including SVG presentation attributes like `stroke`/`fill` — exactly like `RadialProgress` does it in production: `stroke={theme.colors['background-current']}` (`packages/gamut/src/RadialProgress/index.tsx`). Reach for this first; it's simpler and it's what real Gamut components actually do for imperative SVG color.
**For colors, `theme.colors[key]` is usually enough — it works with aliases and doesn't need a second lookup step.** `theme.colors[key]` (including semantic aliases like `'primary'` or `'background-current'`) returns a live CSS variable reference (`var(--color-key)`), which the browser resolves through the normal light/dark cascade. This works as a plain attribute value on DOM-rendered elements — including SVG presentation attributes like `stroke`/`fill` — exactly like `RadialProgress` does it in production: `stroke={theme.colors['background-current']}` (`packages/gamut/src/RadialProgress/index.tsx`). Reach for this first; it's simpler and it's what real Gamut components actually do for imperative SVG color.

**The exception**: a `var(...)` string doesn't mean anything to a consumer outside the DOM/CSS rendering pipeline — canvas 2D `fillStyle`, WebGL, or any color-math (contrast calculations, blending) that needs the actual resolved value. For those, use `useColorModes()` instead of bare `useTheme()` — but its `getColorValue()` only accepts a **raw palette key** (`hyper-500`, `navy-800`, …), not an alias: `getColorValue('primary')` silently returns `undefined`, since aliases are never merged into the raw-key map `getColorValue` reads. Resolve the alias first with `isColorAlias`, exactly like `packages/gamut-styles/src/Background.tsx` does before computing contrast:

Expand Down
10 changes: 5 additions & 5 deletions packages/gamut-brand/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ That writes `src/theme/acme/`:
| File | What it is |
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `values.ts` | The palette, roles, content modes and the brand's CSS as typed constants. `createBrandTheme` takes this module |
| `tokens.css` | `--colors-*` and `--gradient-*` custom properties for this brand only, starting on `:root` in its initial mode |
| `tokens.css` | `--color-*` and `--gradient-*` custom properties for this brand only, starting on `:root` in its initial mode |
| `palette.tokens.json`, `light.tokens.json`, `dark.tokens.json` | DTCG 2025.10 for Figma and other tools. Each color is OKLCH with its hex alongside |

Check the files in, like any generated source. Then build the theme from them:
Expand Down Expand Up @@ -175,12 +175,12 @@ yarn workspace @skillsoft/gamut-styles validate:tokens # full report for gamut

## Naming

- Primitives: `--colors-palette-<slot>-<grade>`, for example `--colors-palette-primary-800`. Not for components; use roles.
- Translucent primitives: `--colors-palette-primary-600-a6` (6% alpha), `--colors-black-a60`.
- Roles: `--colors-<role>`, for example `--colors-action-primary`.
- Primitives: `--color-palette-<slot>-<grade>`, for example `--color-palette-primary-800`. Not for components; use roles.
- Translucent primitives: `--color-palette-primary-600-a6` (6% alpha), `--color-black-a60`.
- Roles: `--color-<role>`, for example `--color-action-primary`.
- `white` and `black` keep their bare names (boundary tokens).

The `--colors-` prefix on primitives is there because variance's `addColors` always emits `--colors-<key>`. One name for both the Emotion and CSS routes beat the shorter `--palette-*`.
The `--color-` prefix on primitives is there because variance's `addColors` always emits `--color-<key>`. One name for both the Emotion and CSS routes beat the shorter `--palette-*`.

## Known gaps

Expand Down
2 changes: 1 addition & 1 deletion packages/gamut-brand/src/__tests__/system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe('color system', () => {
'primary',
]);
expect(gradientCssVars(ocd.gradients.light['surface-brand'])).toContain(
'radial-gradient(circle calc(0.85 * 100cqw) at 100% -5%, color-mix(in oklab, var(--colors-palette-accent-alt-300)'
'radial-gradient(circle calc(0.85 * 100cqw) at 100% -5%, color-mix(in oklab, var(--color-palette-accent-alt-300)'
);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/gamut-brand/src/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const gradientCssVars = (gradient: Gradient) =>
.map(({ ref, radius, at }) => {
const stops = FALLOFF.map(
([pos, alpha]) =>
`color-mix(in oklab, var(--colors-${paletteKey(ref)}) ${Number(
`color-mix(in oklab, var(--color-${paletteKey(ref)}) ${Number(
(alpha * 100).toFixed(2)
)}%, transparent) ${pos}%`
).join(', ');
Expand Down
10 changes: 5 additions & 5 deletions packages/gamut-brand/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ const FORMAT = 'css/gamut-variables';
*
* Mode blocks also get:
* - `color-scheme`, so native controls and scrollbars follow the mode;
* - `color: var(--colors-text-default)`, because `color` inherits as a computed
* - `color: var(--color-text-default)`, because `color` inherits as a computed
* value — without it, text under a nested `[data-color-mode]` keeps the
* parent mode's color (ColorMode sets it on its wrapper for the same reason);
* - a default `--colors-background-current`, which ColorMode sets at runtime
* - a default `--color-background-current`, which ColorMode sets at runtime
* and a CSS-only subtree needs from somewhere.
*/
StyleDictionary.registerFormat({
Expand All @@ -68,8 +68,8 @@ StyleDictionary.registerFormat({
const modeLines = mode
? [
` color-scheme: ${mode};`,
' color: var(--colors-text-default);',
' --colors-background-current: var(--colors-surface-default);',
' color: var(--color-text-default);',
' --color-background-current: var(--color-surface-default);',
]
: [];
return `${header}${options.selector} {\n${[
Expand All @@ -96,7 +96,7 @@ const cssBlock = async (
platforms: {
css: {
transforms: ['name/kebab', 'color/oklch'],
prefix: 'colors',
prefix: 'color',
files: [
{
destination: 'unused.css',
Expand Down
6 changes: 3 additions & 3 deletions packages/gamut-brand/src/roundTrip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const parseTokensCss = (
slot,
Object.fromEntries(
GRADES.map((grade) => {
const p = oklch(palette[`--colors-palette-${slot}-${grade}`]);
const p = oklch(palette[`--color-palette-${slot}-${grade}`]);
return [grade, { grade, l: p.l, c: p.c, h: p.h, hex: '' }];
})
),
Expand All @@ -61,12 +61,12 @@ export const parseTokensCss = (
Object.fromEntries(
ROLES.map((r) => {
const ref =
decls[`--colors-${r.token}`]?.match(/^var\((--[\w-]+)\)$/);
decls[`--color-${r.token}`]?.match(/^var\((--[\w-]+)\)$/);
if (!ref) throw new Error(`${r.token} (${mode}) is not a var()`);
const value: ResolvedValue = {
...oklch(palette[ref[1]]),
hex: '',
key: ref[1].replace(/^--colors-/, ''),
key: ref[1].replace(/^--color-/, ''),
};
return [r.token, value];
})
Expand Down
5 changes: 4 additions & 1 deletion packages/gamut-docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ export default defineConfig({
baseUrl:
'https://github.com/Codecademy/skillsoft-gamut/edit/main/packages/gamut-docs/',
},
customCss: ['./src/styles/gamut-core-theme.css'],
customCss: [
'@skillsoft/gamut-styles/tokens.css',
'./src/styles/gamut-theme.css',
],
plugins: [
starlightSidebarTopics(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Use this for a single, high-visibility marketing action — the offset shadow an
- **Anything other than the single most important action on the page** — use [FillButton](/components/actions/buttons/fill-button/) instead.
- **An icon alongside the label** — `CTAButton` doesn't support the `icon`/`iconPosition` props the other buttons do; use [FillButton](/components/actions/buttons/fill-button/) if an inline icon is required.
- **Icon-only actions** — use [IconButton](/components/actions/buttons/icon-button/) instead; it requires an accessible name via its `tip` prop.
- **Outside its color-mode context** — don't place a light-mode button on a navy background; wrap the surface in `Background` instead of styling around it.
- **Outside its color-mode context** — don't place a light-mode button on a dark background; wrap the surface in `Background` instead of styling around it.

### Anatomy

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Use this for primary actions. Include a leading or trailing icon from our mini i
- **Low-emphasis, inline actions** — use [TextButton](/components/actions/buttons/text-button/) instead.
- **Icon-only actions** — use [IconButton](/components/actions/buttons/icon-button/) instead; it requires an accessible name via its `tip` prop.
- **Marketing / high-visibility CTAs** — use [CTAButton](/components/actions/buttons/cta-button/) instead.
- **Outside its color-mode context** — don't place a light-mode button on a navy background; wrap the surface in `Background` instead of styling around it.
- **Outside its color-mode context** — don't place a light-mode button on a dark background; wrap the surface in `Background` instead of styling around it.

### Anatomy

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Use this for secondary or space-constrained actions with a recognizable icon and

- **A visible label fits** — use [TextButton](/components/actions/buttons/text-button/), [StrokeButton](/components/actions/buttons/stroke-button/), or [FillButton](/components/actions/buttons/fill-button/) instead; don't reach for an icon-only button just to save space if a label would fit.
- **Marketing / high-visibility CTAs** — use [CTAButton](/components/actions/buttons/cta-button/) instead.
- **Outside its color-mode context** — don't place a light-mode button on a navy background; wrap the surface in `Background` instead of styling around it.
- **Outside its color-mode context** — don't place a light-mode button on a dark background; wrap the surface in `Background` instead of styling around it.

### Anatomy

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Use this for secondary actions. Include a leading or trailing icon from our mini
- **Low-emphasis, inline actions** — use [TextButton](/components/actions/buttons/text-button/) instead.
- **Icon-only actions** — use [IconButton](/components/actions/buttons/icon-button/) instead; it requires an accessible name via its `tip` prop.
- **Marketing / high-visibility CTAs** — use [CTAButton](/components/actions/buttons/cta-button/) instead.
- **Outside its color-mode context** — don't place a light-mode button on a navy background; wrap the surface in `Background` instead of styling around it.
- **Outside its color-mode context** — don't place a light-mode button on a dark background; wrap the surface in `Background` instead of styling around it.

### Anatomy

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Use this for tertiary actions. Include a leading or trailing icon from our mini
- **Secondary / outlined actions** — use [StrokeButton](/components/actions/buttons/stroke-button/) instead.
- **Icon-only actions** — use [IconButton](/components/actions/buttons/icon-button/) instead; it requires an accessible name via its `tip` prop.
- **Marketing / high-visibility CTAs** — use [CTAButton](/components/actions/buttons/cta-button/) instead.
- **Outside its color-mode context** — don't place a light-mode button on a navy background; wrap the surface in `Background` instead of styling around it.
- **Outside its color-mode context** — don't place a light-mode button on a dark background; wrap the surface in `Background` instead of styling around it.

### Anatomy

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Card
description: A contained surface with background color variants, optional shadow/pattern treatments, and hover behavior.
description: A contained surface with two variants, optional shadow/pattern treatments, and hover behavior.
---

import FigmaEmbed from '~/components/FigmaEmbed.astro';
Expand All @@ -14,8 +14,8 @@ Use `Card` to present a piece of information as a distinct, contained surface, w

**Best practices:**

- Use `variant="default"` (the default) for a background that follows the current color mode automatically; use `white`/`yellow`/`beige` inside a light-mode surface, or `navy`/`hyper` inside a dark-mode surface, when a static color is what's needed.
- Add `isInteractive` when wrapping `Card` in an `Anchor` — it applies a `'md'` border radius and a hover shadow effect, and a card with a pattern loses its pattern on hover.
- Use `variant="default"` (the default) for a card on `surface-raised`, which follows the color mode. Use `variant="brand"` for a featured card on the brand gradient. Every card has a `border-subtle` hairline. The old color variants (`white`, `beige`, `navy`, `yellow`, `hyper`) are gone; for a dark card, wrap it in `<ColorMode mode="dark">`.
- Add `isInteractive` when wrapping `Card` in an `Anchor`. It applies an `'md'` border radius and the theme's raised shadow, and on hover a `state-hover` layer and a deeper shadow; a card with a pattern loses its pattern on hover. A card that isn't interactive has no shadow.
- `borderRadius` defaults to `'none'` (non-interactive) or `'md'` (`isInteractive`), and can be overridden — the outline/pattern shadow automatically matches whatever `borderRadius` is set.

**When NOT to use:**
Expand All @@ -34,25 +34,23 @@ The shadow/pattern treatment (`shadow`) always renders along the bottom edge plu

Wrap `Card` in an `Anchor` and add `isInteractive` to get the hover shadow and `'md'` border radius:

<StoryEmbed id="atoms-card--is-interactive" height="14rem" />
<StoryEmbed id="containers-card--is-interactive" height="14rem" />

## Prop Reference

### Variant

<StoryEmbed id="atoms-card--dynamic" height="12rem" />
<StoryEmbed id="atoms-card--static-light" height="10rem" />
<StoryEmbed id="atoms-card--static-dark" height="10rem" />
<StoryEmbed id="containers-card--variants" height="12rem" />
<StoryEmbed id="containers-card--dynamic" height="12rem" />

### Shadow

<StoryEmbed id="atoms-card--shadow-none" height="14rem" />
<StoryEmbed id="atoms-card--shadow-outline" height="14rem" />
<StoryEmbed id="atoms-card--shadow-pattern-left" height="14rem" />
<StoryEmbed id="atoms-card--shadow-pattern-right" height="14rem" />
<StoryEmbed id="containers-card--shadow-outline" height="14rem" />
<StoryEmbed id="containers-card--shadow-pattern-left" height="14rem" />
<StoryEmbed id="containers-card--shadow-pattern-right" height="14rem" />

### Border radius

`borderRadius` defaults to `'none'`, or `'md'` when `isInteractive` is set, and can be overridden independently of both:

<StoryEmbed id="atoms-card--border-radius" height="14rem" />
<StoryEmbed id="containers-card--border-radius" height="14rem" />
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Use `BarChart` to visualize comparative data across categories, as a horizontal
4. **Scale/value axis** — the minimum is always 0; dividers and the maximum are adjustable via `scaleInterval`/`maxScaleValue`.
5. **Series one value**
6. **Series two value** (optional, via `seriesTwoValue`) — creates the stacked effect.
7. **Series one bar** — defaults to the text color; gains an automatic white/navy border for contrast when needed.
7. **Series one bar** — defaults to the text color; gains an automatic light or dark border for contrast when needed.
8. **Series two bar** — defaults to the primary color; same automatic border rule.

## Usage examples
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ For best practices specific to one component — like alerts, errors, or confirm

## Prefer semantic tokens over raw values

Access colors through Gamut's semantic aliases (`text`, `background`, `primary`, `secondary`) rather than raw color values — see [Color modes](/concepts/color-modes/) for why. Aliases guarantee you get the right color for the right theme and color mode, with the right types and states.
Access colors through Gamut's color roles (`text-default`, `surface-raised`, `action-primary` and the rest) rather than raw values or palette colors; see [Color modes](/concepts/color-modes/) for why. Roles give you the right color for the theme and color mode, with the right types and states. [Color system](/concepts/color-system/#pick-a-role-by-its-job) lists which role fits which job.

```tsx
import { css } from '@skillsoft/gamut-styles';
Expand All @@ -17,7 +17,7 @@ import styled from '@emotion/styled';
const Box = styled.div(css({ p: 4 }));

// A semantic color, not a raw one
const OtherCoolThing = styled.div(css({ color: 'primary', p: 4 }));
const OtherCoolThing = styled.div(css({ color: 'action-primary', p: 4 }));
```

`variant` and `states` extend the same idea to whole sets of styles — see [Compose and variants](/foundations/system-props/compose-and-variants/) for the full API.
Expand Down
Loading