sillyspace/docs/theme-tokens.md
2026-06-03 22:59:03 -04:00

13 KiB

Theme Tokens

Theme tokens are the internal CSS custom-property contract for the app shell and reusable UI components.

This document is for maintainers changing the app theme system.

Skin authors should use Profile Skins instead.

Do not merge this document into skins.md.

skins.md describes the user-facing skin HTML contract and sanitizer boundary.

This document describes how app and admin themes feed the shared component palette.

Source Files

  • public/static/css/core/tokens.css defines default theme tokens, sizing tokens, and component-facing aliases.
  • public/static/css/themes/dark.css defines the cookie-selected dark override layer.
  • src/theme/colorPalette.ts defines the admin color-palette model and derived color calculations.
  • src/theme/themeCss.ts writes /branding.css from a saved admin color palette.
  • src/routes/system/theme.ts serves /theme.css and handles the light/dark toggle cookie.
  • src/routes/system/branding.ts serves /branding.css.
  • src/server/db/branding.ts stores and reads the admin color palette.
  • src/views/staff/siteSettings.tsx renders the admin color-theme form.
  • public/static/css/features/profile.css maps profile skin variables into the same component-facing aliases on skinned profile pages.

Token Layers

The theme system has four layers.

1. Admin Palette Source

The admin color-theme form edits eight source colors.

These source colors are defined by colorPaletteTokens in src/theme/colorPalette.ts.

Palette token Admin label Default
chrome Header #7c3aed
chromeText Header text #ffffff
accent Accent #7c3aed
accentText Accent text #ffffff
link Link #6b21a8
backdrop Backdrop #e6e3ea
page Page #ffffff
surface Panel #ffffff

The admin form field names are color_chrome, color_chromeText, color_accent, color_accentText, color_link, color_backdrop, color_page, and color_surface.

Only six-digit hex colors are accepted by the palette parser.

Invalid submitted palette values fall back to the current palette value or the default palette.

The saved admin palette is stored as JSON in the branding.palette setting.

Saving the default palette removes the saved branding.palette setting.

2. App Theme Variables

public/static/css/core/tokens.css defines default --app-theme-* fallback values.

src/theme/themeCss.ts writes these --app-theme-* variables into /branding.css when admin branding is customized:

--app-theme-backdrop
--app-theme-page
--app-theme-surface
--app-theme-chrome
--app-theme-chrome-text
--app-theme-accent
--app-theme-accent-text
--app-theme-link
--app-theme-link-hover
--app-theme-surface-link
--app-theme-surface-link-hover
--app-theme-page-text
--app-theme-surface-text
--app-theme-muted

--app-theme-focus exists in the default CSS and is used as the fallback for --theme-focus.

The admin color-theme form does not currently edit --app-theme-focus.

3. Active Theme Variables

Components do not consume admin palette tokens directly.

They consume active --theme-* variables.

Default active theme variables are defined in public/static/css/core/tokens.css.

Customized admin branding writes these active variables into /branding.css:

--theme-backdrop
--theme-page
--theme-surface
--theme-chrome
--theme-chrome-text
--theme-accent
--theme-accent-text
--theme-link
--theme-link-hover
--theme-surface-link
--theme-surface-link-hover
--theme-page-text
--theme-surface-text
--theme-muted

These active theme variables are CSS-only defaults and are not written by src/theme/themeCss.ts:

--theme-danger
--theme-danger-text
--theme-success
--theme-success-ink
--theme-success-text
--theme-focus
--theme-page-grid-a
--theme-page-grid-b
--theme-toggle-dark-display
--theme-toggle-light-display

4. Component-Facing Aliases

Reusable component CSS consumes --color-*, --surface-*, --page-background, and --shadow-* aliases.

Those aliases are resolved for both :root and [data-skin-page] in public/static/css/core/tokens.css.

The shared resolver lets admin themes and profile skins use the same component formulas.

Component-facing color aliases are:

--color-page
--color-canvas
--color-surface
--color-surface-raised
--color-surface-tint
--color-text
--color-text-muted
--color-text-on-bright
--color-link
--color-link-hover
--color-brand
--color-brand-accent
--color-brand-header
--color-brand-nav
--color-brand-soft
--color-brand-border
--color-panel-accent
--color-panel-accent-soft
--color-panel-rule
--color-container-border
--color-danger
--color-danger-text
--color-danger-soft
--color-danger-border
--color-success
--color-success-ink
--color-success-text
--color-success-soft
--color-success-border
--color-field
--color-field-disabled
--color-field-border-light
--color-field-border
--color-field-border-dark
--color-focus
--color-link-focus-bg
--color-row-alt
--color-shadow-panel
--color-shadow-soft
--color-shadow-pressed
--color-shadow-card
--color-button-primary-bg
--color-button-primary-text
--color-button-primary-border-light
--color-button-primary-border-dark
--color-button-primary-hover-bg
--color-button-primary-hover-text
--color-button-secondary-bg
--color-button-secondary-text
--color-button-secondary-border-light
--color-button-secondary-border-dark
--color-button-secondary-hover-bg
--color-button-secondary-hover-text
--color-button-danger-bg
--color-button-danger-text
--color-button-danger-border-light
--color-button-danger-border-dark
--color-button-danger-hover-bg
--color-button-danger-hover-text

Component-facing surface aliases are:

--surface-background
--surface-background-raised
--surface-background-soft
--surface-background-tint
--surface-border
--surface-border-soft
--surface-rule
--surface-shadow

Page and shadow aliases are:

--page-background
--shadow-panel
--shadow-button
--shadow-field
--shadow-pressed
--shadow-photo
--shadow-card
--column-divider-shadow

Derived Colors

src/theme/colorPalette.ts derives readable and hover colors from the eight admin palette source colors.

deriveColorPalette() returns:

accent
accentText
backdrop
chrome
chromeText
muted
page
pageLink
pageLinkHover
pageText
surface
surfaceLink
surfaceLinkHover
surfaceText

pageText and surfaceText are chosen as black or white based on contrast.

muted is mixed from surfaceText and surface.

pageLinkHover and surfaceLinkHover are adjusted toward readable contrast against their background.

The CSS layer derives borders, soft surfaces, fields, shadows, and button border colors with color-mix(...).

Surface Meanings

--theme-backdrop is the browser background behind the app container.

--theme-page is the page canvas inside the app container.

--theme-surface is used for panels, cards, composers, profile actions, and form surfaces.

--theme-chrome is used for navigation, header-like chrome, and panel headings.

--theme-accent is used for primary actions.

--theme-link is the normal content link source color.

--theme-page-text is used for text on the page canvas.

--theme-surface-text is used for text on panels and surfaces.

--theme-muted is used for secondary text.

--theme-focus is used for focus styling.

Default Light Theme

The default light theme is defined in public/static/css/core/tokens.css.

The default theme color source variables such as --theme-chrome, --theme-accent, --theme-link, --theme-backdrop, --theme-page, and --theme-surface point at default --app-theme-* fallback values.

The default --theme-page-grid-a and --theme-page-grid-b variables derive a subtle page-grid background from --theme-backdrop.

Dark Theme

The dark theme is a small override layer in public/static/css/themes/dark.css.

/theme.css imports public/static/css/themes/dark.css only when the app_theme cookie is dark and admin branding is not customized.

The dark override is scoped to :root:not([data-theme-lock="light"]).

Dark mode overrides:

color-scheme
--theme-backdrop
--theme-page
--theme-surface
--theme-page-text
--theme-link
--theme-muted
--theme-danger
--theme-success-text
--theme-toggle-dark-display
--theme-toggle-light-display

The dark override does not write --app-theme-* values.

Admin Branding

Customized admin branding is served from /branding.css.

/branding.css is empty when branding is not customized.

When branding is customized, src/theme/themeCss.ts writes both --app-theme-* fallback values and active --theme-* values to :root.

Customized admin branding disables the light/dark theme toggle in the shell.

Customized admin branding locks the document with data-theme-lock="light" so the dark override does not apply.

Resetting the color theme deletes the saved palette and returns the app to the default light/dark behavior.

Profile Skins

Profile skins do not write this global contract directly.

Skin CSS should use --skin-palette-* variables scoped to [data-skin-page].

public/static/css/features/profile.css maps skin palette variables into page-local --skin-* variables.

public/static/css/features/profile.css then republishes those values through local --theme-* variables on [data-skin-page].

Because component-facing aliases are resolved for [data-skin-page], skinned profile pages use the same component formulas as admin themes.

Missing skin palette variables inherit the current --app-theme-* values.

Profiles without sanitized skin CSS inherit the normal site theme.

The skin sanitizer drops custom properties whose names start with --theme-, --app-theme-, or --color-.

Skin authors should use the public skin variables and hooks documented in Profile Skins.

Profile skin radius variables map into shared radius tokens on [data-skin-page]:

Skin variable Shared token
--skin-radius-panel --radius-panel
--skin-radius-photo --radius-media
--skin-radius-control --radius-subtle

--skin-radius is a fallback used by the skin radius variables.

Non-Color Tokens

public/static/css/core/tokens.css also defines non-color tokens.

These tokens are not controlled by the admin color-theme form.

Base color constants:

--color-white
--color-ink

Typography tokens:

--font-body
--line-height-body
--font-size-main
--font-size-footer
--font-size-note
--font-size-caption
--font-size-small
--font-size-nav
--font-size-nav-link
--font-size-utility-action
--font-size-card-heading
--font-size-panel-heading
--font-size-profile-heading

Layout and size tokens:

--container-width
--container-min-height
--content-measure
--nav-side-min
--audio-width
--avatar-size
--avatar-compact-size
--profile-photo-size
--profile-edit-photo-size
--person-card-width
--person-card-min-height
--profile-person-card-width
--details-label-width
--discussion-author-width
--column-compact
--column-profile
--column-wide
--column-aside-min
--profile-sidebar-min

Spacing tokens:

--space-1
--space-2
--space-3
--space-4
--space-5
--space-6
--space-7
--space-8
--space-9
--space-10
--space-11
--space-12
--layout-stack-gap
--panel-body-gap
--list-stack-gap
--space-main-y
--space-main-x
--space-nav-links-y
--space-nav-links-x
--link-separator-gap
--article-title-offset
--icon-label-gap
--icon-offset-y

Border, radius, control, and table tokens:

--border-thin
--border-medium
--radius-panel-default
--radius-media-default
--radius-panel
--radius-media
--radius-control-default
--radius-subtle
--text-decoration-hairline
--text-underline-offset
--icon-size
--brand-icon-size
--avatar-glyph-size
--control-min-height
--control-pad-y
--control-pad-x
--button-active-shift
--disabled-opacity
--focus-outline-width
--focus-offset
--table-cell-y
--table-cell-x
--contact-cell-min-height
--nav-search-width
--nav-link-min-height

Maintainer Rules

Add a new admin-controlled source color only if it belongs in the eight-token color palette model or if that model is intentionally expanded.

If the admin palette model changes, update src/theme/colorPalette.ts, src/theme/themeCss.ts, the admin color form, and this document.

If a component needs a reusable color, prefer adding a component-facing alias in public/static/css/core/tokens.css rather than reading --app-theme-* directly.

If a profile skin should control a new theme concept, add a --skin-palette-* or --skin-* token in public/static/css/features/profile.css and update Profile Skins.

Do not require skin authors to set --theme-*, --app-theme-*, or --color-*.

Change Checklist

When changing theme tokens:

  • update public/static/css/core/tokens.css;
  • update src/theme/colorPalette.ts and src/theme/themeCss.ts if admin branding should control the token;
  • update the admin color-theme form if admins need to edit the token;
  • keep public/static/css/themes/dark.css as a small override layer;
  • verify /branding.css output when branding is customized;
  • verify /theme.css output with and without the dark cookie;
  • check profile skins still use their separate skin contract;
  • update Profile Skins when skin-facing variables change.