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].

palette

Primitives

Raw values

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-950
contrast

Tokens

[data-theme]

Theme-reactive tokens that components consume. Swap between light and dark via the data-theme attribute on the html root.

backgroundforegroundprimarysecondarytertiarycontentcontent-secondary
brush

Apps

[data-app]

Brand accent that swaps per LoopOS app. Override via the data-app attribute. Defaults to Core.

appapp-fgapp-gradient-lightapp-gradient-dark
bookmark

Semantic

Static

Status colors pinned to fixed values. Never react to theme or app — always the same. Used for feedback and status communication.

successdangerwarninginformative…-fg
widgets

UI Elements

Applied layer

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.

CSS·Define + override tokens src/app/globals.css
@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"].

TSX
<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.

TSX
<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"
Solid#e14a00
Light#fcaa30
Dark#f27335

Validation

data-app="validation"
Solid#009e8c
Light#00d8b6
Dark#059898

Handling

data-app="handling"
Solid#6f4cff
Light#8044ff
Dark#443092

Hubs

data-app="hubs"
Solid#0b6bff
Light#00c5ff
Dark#0072ff

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.

TSX
<Badge variant="success">Live</Badge>
<Badge variant="danger">Failed</Badge>
<Badge variant="warning">Throttled</Badge>
<Badge variant="informative">Beta</Badge>
#0fd877
#0d6e41

Success

successsuccess-fg
#f83446
#a01421

Danger

dangerdanger-fg
#ef9a11
#8f3f11

Warning

warningwarning-fg
#068bee
#024a8a

Informative

informativeinformative-fg

Primitives · 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