Colors
Four layers — primitives, semantic, theme tokens, and per-app brand. Components only ever consume the top three. Sections below run from what you use most (theme tokens) down to the raw values.
Architecture
Color token architecture
Four layers, two switches. Primitives feed the three middle layers; components only ever touch the applied layer. Theme reactivity is encoded in the layer — semantic stays fixed, tokens flip with [data-theme], apps flip with [data-app].
Primitives
The 12-step gray scale that the Tokens layer references. Don't apply primitives directly to elements — that's what the layers below are for.
gray-25 → gray-950Tokens
Theme-reactive tokens that components consume. Swap between light and dark via the data-theme attribute on the html root.
backgroundforegroundprimarysecondarytertiarycontentcontent-secondaryApps
Brand accent that swaps per LoopOS app. Override via the data-app attribute. Defaults to Core.
appapp-fgapp-gradient-lightapp-gradient-darkSemantic
Status colors pinned to fixed values. Never react to theme or app — always the same. Used for feedback and status communication.
successdangerwarninginformative…-fgUI Elements
The only layer touched in component code. Reference tokens from the three layers above via Tailwind utilities or var(--color-*) — never primitives or raw hex.
<div className="bg-background text-content border-tertiary">…Setup
Wire it up
The complete color system in plain CSS — drop it into your stylesheet pipeline (any framework). Then set data-theme and data-app on the document root: data-theme="light"|"dark" flips the Tokens layer, data-app="core"|"validation"|"handling"|"hubs" flips the Apps layer.
@import "tailwindcss";
@theme {
/* ─── PRIMITIVES — raw values, never used directly ─── */
--color-primitive-gray-25: #fcfcfc;
--color-primitive-gray-50: #f6f6f6;
--color-primitive-gray-100: #e7e7e7;
--color-primitive-gray-200: #d1d1d1;
--color-primitive-gray-300: #b0b0b0;
--color-primitive-gray-400: #888888;
--color-primitive-gray-500: #6d6d6d;
--color-primitive-gray-600: #5d5d5d;
--color-primitive-gray-700: #4f4f4f;
--color-primitive-gray-800: #454545;
--color-primitive-gray-900: #3d3d3d;
--color-primitive-gray-950: #181818;
--color-primitive-app-core-solid: #e14a00;
--color-primitive-app-core-gradient-light: #fcaa30;
--color-primitive-app-core-gradient-dark: #f27335;
--color-primitive-app-validation-solid: #009e8c;
--color-primitive-app-validation-gradient-light: #00d8b6;
--color-primitive-app-validation-gradient-dark: #059898;
--color-primitive-app-handling-solid: #6f4cff;
--color-primitive-app-handling-gradient-light: #8044ff;
--color-primitive-app-handling-gradient-dark: #443092;
--color-primitive-app-hubs-solid: #0b6bff;
--color-primitive-app-hubs-gradient-light: #00c5ff;
--color-primitive-app-hubs-gradient-dark: #0072ff;
--color-primitive-semantic-success-500: #0fd877;
--color-primitive-semantic-success-800: #0d6e41;
--color-primitive-semantic-danger-500: #f83446;
--color-primitive-semantic-danger-800: #a01421;
--color-primitive-semantic-warning-500: #ef9a11;
--color-primitive-semantic-warning-800: #8f3f11;
--color-primitive-semantic-informative-500: #068bee;
--color-primitive-semantic-informative-800: #024a8a;
/* ─── SEMANTIC — static, never react to theme ─── */
--color-success: var(--color-primitive-semantic-success-500);
--color-success-fg: var(--color-primitive-semantic-success-800);
--color-danger: var(--color-primitive-semantic-danger-500);
--color-danger-fg: var(--color-primitive-semantic-danger-800);
--color-warning: var(--color-primitive-semantic-warning-500);
--color-warning-fg: var(--color-primitive-semantic-warning-800);
--color-informative: var(--color-primitive-semantic-informative-500);
--color-informative-fg: var(--color-primitive-semantic-informative-800);
/* ─── TOKENS — light defaults, react to [data-theme] ─── */
--color-background: #ffffff;
--color-foreground: var(--color-primitive-gray-800);
--color-primary: var(--color-primitive-gray-25);
--color-content: var(--color-primitive-gray-950);
--color-secondary: var(--color-primitive-gray-50);
--color-tertiary: var(--color-primitive-gray-100);
--color-content-secondary: var(--color-primitive-gray-500);
--color-shimmer-highlight: rgba(255, 255, 255, 0.75);
/* ─── APPS — defaults to Core, react to [data-app] ─── */
--color-app: var(--color-primitive-app-core-solid);
--color-app-fg: #ffffff;
--color-app-gradient-light: var(--color-primitive-app-core-gradient-light);
--color-app-gradient-dark: var(--color-primitive-app-core-gradient-dark);
}
/* Theme overrides */
[data-theme="light"] {
--color-background: #ffffff;
--color-foreground: var(--color-primitive-gray-800);
--color-primary: var(--color-primitive-gray-25);
--color-content: var(--color-primitive-gray-950);
--color-secondary: var(--color-primitive-gray-50);
--color-tertiary: var(--color-primitive-gray-100);
--color-content-secondary: var(--color-primitive-gray-500);
--color-shimmer-highlight: rgba(255, 255, 255, 0.75);
}
[data-theme="dark"] {
--color-background: var(--color-primitive-gray-950);
--color-foreground: var(--color-primitive-gray-300);
--color-primary: #202020;
--color-content: var(--color-primitive-gray-25);
--color-secondary: #242424;
--color-tertiary: #2e2e2e;
--color-content-secondary: var(--color-primitive-gray-400);
--color-shimmer-highlight: rgba(255, 255, 255, 0.08);
}
/* App overrides */
[data-app="validation"] {
--color-app: var(--color-primitive-app-validation-solid);
--color-app-gradient-light: var(--color-primitive-app-validation-gradient-light);
--color-app-gradient-dark: var(--color-primitive-app-validation-gradient-dark);
}
[data-app="handling"] {
--color-app: var(--color-primitive-app-handling-solid);
--color-app-gradient-light: var(--color-primitive-app-handling-gradient-light);
--color-app-gradient-dark: var(--color-primitive-app-handling-gradient-dark);
}
[data-app="hubs"] {
--color-app: var(--color-primitive-app-hubs-solid);
--color-app-gradient-light: var(--color-primitive-app-hubs-gradient-light);
--color-app-gradient-dark: var(--color-primitive-app-hubs-gradient-dark);
}Tokens · [data-theme]
Application tokens
The layer your components consume. These flip with the data-theme attribute on the html root — light values are defaults, dark values override inside [data-theme="dark"].
<div className="bg-background text-content border border-tertiary rounded-xl p-4">
<h3 className="copy-16-medium">Card title</h3>
<p className="copy-14 text-content-secondary">Body copy uses the secondary text token.</p>
</div>Light
[data-theme="light"]Background
Primary
Secondary
Tertiary
Content
Content Secondary
Foreground
Dark
[data-theme="dark"]Background
Primary
Secondary
Tertiary
Content
Content Secondary
Foreground
Apps · [data-app]
App brand colors
Each LoopOS app has a solid accent and a gradient pair. Switched per surface via the data-app attribute. Defaults to Core.
<button className="bg-app text-app-fg rounded-md px-3 py-1.5">
Continue
</button>
<div data-app="hubs" className="bg-app text-app-fg p-4">
This block now uses the Hubs accent regardless of the page-level app.
</div>Live preview
bg-app
Solid accent
gradient-light → gradient-dark
Gradient surface
Core
data-app="core"Validation
data-app="validation"Handling
data-app="handling"Hubs
data-app="hubs"Semantic · Static
Status colors
Fixed feedback colors. Don't react to theme or app — the same hex everywhere. Pair the surface tone with its -fg counterpart.
Use the Badge component when possible — see src/components/ui/Badge.tsx.
<Badge variant="success">Live</Badge>
<Badge variant="danger">Failed</Badge>
<Badge variant="warning">Throttled</Badge>
<Badge variant="informative">Beta</Badge>Success
successsuccess-fgDanger
dangerdanger-fgWarning
warningwarning-fgInformative
informativeinformative-fgPrimitives · Raw values
Gray scale
Foundation values referenced by the layers above. Never apply directly to elements — if you reach for a primitive in component code, you're missing a token.
25
#fcfcfc
50
#f6f6f6
100
#e7e7e7
200
#d1d1d1
300
#b0b0b0
400
#888888
500
#6d6d6d
600
#5d5d5d
700
#4f4f4f
800
#454545
900
#3d3d3d
950
#181818