@8x8/oxygen-theme
v3.0.0
Published
Oxygen - Theme
Keywords
Readme
8x8 Oxygen - Theme
This package holds theme definitions and color palettes.
Usage
You don't need to use this. Instead make of of the OxygenProvider component, which exposes the theme object directly to your components.
Please look into @8x8/oxygen-config instead.
You can use the example pages to see the available colors and tokens.
Semantic tokens
Alongside the primitive tokens (action01, ui06, error01, ...) the theme exposes an intent-named semantic layer at theme.semanticColor.*. Semantic tokens resolve to the same hex values as the primitives they map to — they exist so component styles can declare intent ("use the danger-feedback color") rather than position on a scale ("use error01").
// Primitive (still supported)
color: ${({ theme }) => theme.error01};
// Semantic (recommended for new code where covered by the spec)
color: ${({ theme }) => theme.semanticColor.feedback_label_text_danger};Both produce the same rendered pixels. Light and dark themes resolve the same semantic to different primitives where appropriate; theme.semanticColor.bgSurface is #FFFFFF in light and #171719 in dark, because the underlying ui06 primitive differs per theme.
How the layer is structured
| What | Where |
|---|---|
| Spec (design's source of truth) | semantic-mapping.json at the package root |
| Generated runtime mapping | src/semantic/color.ts |
| Generated TS interface | src/semantic/types.ts (exports SemanticColor) |
| Runtime resolver | src/semantic/buildSemanticColor.ts |
| Generator | scripts/generateSemantic.ts |
| Validator | scripts/validateDescriptions.ts |
Naming convention
The spec uses dotted names; the JS identifier is the dotted path with the leading color. stripped and remaining . replaced with _:
| Spec key | JS identifier on theme.semanticColor |
|---|---|
| color.bgSurface | bgSurface |
| color.bgSurface.inverse | bgSurface_inverse |
| color.icon.onBgSurface.inverse | icon_onBgSurface_inverse |
| color.feedback.label.text.danger | feedback_label_text_danger |
The dotted spec name is preserved as a @semanticName JSDoc tag on every entry, so IDE hover always shows the canonical name.
Generating the semantic layer
The semantic files under src/semantic/ are generated — do not edit by hand. Re-run the generator whenever semantic-mapping.json changes:
yarn workspace @8x8/oxygen-theme run generate:semanticThis rewrites src/semantic/color.ts and src/semantic/types.ts. Commit the regenerated files alongside the spec change.
The generator also runs automatically as part of prebuild, followed by validateDescriptions. The validator fails the build when:
- Any token (primitive or semantic) lacks a JSDoc description.
- Any
semantic-mapping.jsonentry has no description or nooxygenToken. - A semantic entry's
oxygenTokendoes not exist onTheme. - A semantic's underscore-flat JS key collides with an existing primitive on
Theme. - The committed
src/semantic/*files are out of date with the live spec (forgot to re-run the generator).
Resolution model & forward-looking metadata
Today every semantic entry has three reference fields:
{
"oxygenToken": "ui06",
"lightPaletteToken": "white",
"darkPaletteToken": "purple01"
}At runtime the resolver uses only oxygenToken to look up the value (theme[oxygenToken]). The two *PaletteToken fields look redundant — they're not. They are forward-looking metadata for the resolution model we expect to grow into:
| Resolution mode | Status | What the resolver reads | When to use |
|---|---|---|---|
| Named primitive (oxygenToken) | Active | theme[oxygenToken] | Every entry today. Goes through the named layer (action01, ui06, error01, ...). |
| Palette-direct (lightPaletteToken + darkPaletteToken) | Reserved | colorPalette[lightPaletteToken] / colorPalette[darkPaletteToken] per theme | A future entry whose intent doesn't have — or shouldn't depend on — a named primitive equivalent. Bypasses the named layer entirely. |
Why this matters. Today's tight coupling between semantics and named primitives means that any future restructuring of the named layer (deprecating error01, renaming ui06, etc.) would force every dependent semantic to be migrated in lockstep. By carrying palette-token metadata on every entry from day one, design retains the option to publish entries that skip the named layer and resolve straight to a palette color.
The current validator already checks that lightPaletteToken / darkPaletteToken exist on colorPalette when present — so they can't silently rot. When the resolver is extended to consume them, no spec migration is needed; the fields are already there.
For now, treat the palette tokens as reference metadata: useful in Storybook and MCP for showing the developer where a color comes from on the palette ramp, and a green-light for future resolution flexibility.
Adding or changing semantic tokens
- Edit
semantic-mapping.json(this is design's contract — coordinate with design before changing). - Run
yarn workspace @8x8/oxygen-theme run generate:semantic. - Commit
semantic-mapping.jsonand the regeneratedsrc/semantic/*files together. - The next
yarn buildruns the generator + validator automatically and confirms integrity.
What semantics do NOT do
- They do not emit CSS variables. The semantic layer is JS-only. CSS variables continue to cover the primitive layer (
--ui06,--text-color01, ...). - They do not replace primitives. Primitives stay exported with the same names and values forever; semantic adoption is opportunistic per component.
- They do not cover spacing / motion / radius / elevation / typography yet — only color. When design publishes mappings for additional categories, the generator picks them up as sibling namespaces (
theme.semanticSpacing.*,theme.semanticMotion.*, etc.) with no consumer-side migration.
