Skip to main content
Casca
Theme

Casca ThemesLink to section

Casca ships four named moods that recolor every primitive at once. Each mood is a small --casca-* token overlay; the rest of the library stays the same.

Built-In MoodsLink to section

MoodLight surfaceLight accentDark surfaceDark accent
Solar#F2EBDA#C8633A#1C1812#E68A60
Cyber#E8F0E9#1E7A4F#0B0E0A#80FF8A
Lunar#E8E6F0#5D44C7#0F1820#B9A6FF
Arcade#FFF1E8#C8246F#191028#FF4A6B

Every mood has a matched dark palette that the OS triggers via prefers-color-scheme: dark. The user picks the mood; the OS picks the brightness.

Canonical mood names map to the bundled source files as follows:

MoodSource file
Solarsrc/themes/default.css
Cybersrc/themes/high-contrast.css
Lunarsrc/themes/muted.css
Arcadesrc/themes/vibrant.css

For <casca-layout>, the layout host must carry class="casca" so the mood overlays and no-JS picker preview can repaint the shadow-hosted chrome.

Prose Callout PaletteLink to section

Prose callouts do not require new theme tokens. The default role colors fall through the existing series and grayscale tokens:

CalloutDefault role token
Note--casca-gray-7 light, --casca-gray-3 dark
Tip--casca-color-2
Warning--casca-color-3
Danger--casca-color-8
Important--casca-color-1

Those defaults stay in-palette for Solar, Cyber, Lunar, and Arcade, but they are mood colors rather than universal semantic colors. A host theme may override --casca-prose-callout-note, --casca-prose-callout-tip, --casca-prose-callout-warning, --casca-prose-callout-danger, or --casca-prose-callout-important on .casca-prose without changing the HTML contract.

Picker MarkupLink to section

dist/casca.css carries Solar as the base mood plus manifest-registered overlays for the other moods. The no-JS picker is a GET form:

<form class="casca-theme-picker" data-casca-theme-picker method="get" action="/_casca/theme">
  <input type="hidden" name="return_to" value="/docs/">
  <fieldset class="casca-theme-picker-fieldset" aria-label="Theme">
    <legend class="sr-only">Theme</legend>
    <label class="casca-theme-picker-button" data-mood="solar" style="--_swatch-a:#f2ebda;--_swatch-b:#c8633a">
      <input type="radio" name="casca-theme" value="solar" checked>
      <span class="sr-only">Solar</span>
    </label>
    <label class="casca-theme-picker-button" data-mood="cyber" style="--_swatch-a:#e8f0e9;--_swatch-b:#1e7a4f">
      <input type="radio" name="casca-theme" value="cyber">
      <span class="sr-only">Cyber</span>
    </label>
  </fieldset>
</form>

Checking a radio previews the mood on the current page in browsers with :has(). The form wrapping is forward-compatibility scaffolding for the future casca serve persistence endpoint; until that ships, the picker is preview-only and selection auto-applies via CSS without a submit step.

PersistenceLink to section

casca dev and casca preview serve the picker action when a theme-picker widget manifest is configured. The request-time state order is:

  1. ?casca-theme=<id> for a one-request URL override.
  2. The casca-theme=<id> cookie for durable preference.
  3. [theme_state].default from the manifest.

Unknown ids fall back to the manifest default. HTML responses are transformed at request time, so the files written by casca site are unchanged. The serving layer sets body[data-casca-theme="<id>"], refreshes the checked picker radio, updates the hidden return_to input to the current path plus query, and emits Vary: Cookie on transformed HTML responses.

The setter endpoint is GET /_casca/theme. It accepts the manifest parameter, currently casca-theme, and optional return_to. A valid id redirects to the validated same-origin return path and sets:

Set-Cookie: casca-theme=<id>; Path=/; SameSite=Lax; HttpOnly; Max-Age=31536000

Secure is added when the local server is running over HTTPS. An empty casca-theme= clears the cookie with Max-Age=0. The cookie is host-only because Domain is omitted.

return_to must be a same-origin path beginning with /. Protocol-relative URLs, absolute scheme URLs, CR, LF, NUL, backslash, over-length values, and the setter path itself fall back to /.

The setter rejects cross-origin writes with Fetch Metadata when available: Sec-Fetch-Site: same-origin and none are allowed, while same-site and cross-site are rejected. If Fetch Metadata is absent and Referer is present, the Referer origin must match the request origin. The response also sets X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none'.

Production Serving RecipesLink to section

Casca still publishes static files. Hosts that need durable theme persistence can add a small edge worker or reverse proxy in front of those files:

  1. Read the manifest ids from deployment config or copy them from site/casca-themes.toml.
  2. On GET /_casca/theme, validate casca-theme, validate return_to, set or clear the host-only cookie with the attributes above, and redirect.
  3. On HTML responses, resolve query override, cookie, or default, then parse the HTML and set body[data-casca-theme], the checked picker radio, and Vary: Cookie or a narrower cache key for the theme cookie.
  4. Bypass assets, feeds, sitemaps, HMR routes, and other reserved operational paths.

Strict static-only hosts can publish per-mood URL spaces such as /solar/..., /cyber/..., /lunar/..., and /arcade/.... Casca does not generate those spaces in 4.7b because they change URLs and canonical policy.

Scoping SelectorsLink to section

Non-base moods are scoped through two roots. Server state comes first and does not depend on :has():

body[data-casca-theme="cyber"].casca,
body[data-casca-theme="cyber"] .casca {
  /* Cyber overrides */
}

Current-page preview is guarded by @supports selector(:has(*)):

@supports selector(:has(*)) {
  body:has(.casca-theme-picker[data-casca-theme-picker] input[name="casca-theme"][value="cyber"]:checked).casca,
  body:has(.casca-theme-picker[data-casca-theme-picker] input[name="casca-theme"][value="cyber"]:checked) .casca {
    /* Cyber overrides */
  }
}

The .casca-theme-picker[data-casca-theme-picker] ancestor and name="casca-theme" input selector prevent unrelated page forms from hijacking mood state.

Theme ManifestLink to section

The all-in-one bundle is assembled from a TOML manifest:

[theme_state]
param = "casca-theme"
cookie = "casca-theme"
picker_root = ".casca-theme-picker[data-casca-theme-picker]"
setter_path = "/_casca/theme"
default = "solar"

[[themes]]
id = "solar"
label = "Solar"
kind = "css"
source = "../src/themes/default.css"
base = true
swatch_light = ["#f2ebda", "#c8633a"]
swatch_dark = ["#251e16", "#e68a60"]

[[themes]]
id = "ember"
label = "Ember"
kind = "tokens"
source = "themes/ember.toml"
swatch_light = ["#fff2df", "#b94e2f"]
swatch_dark = ["#211713", "#ff9a68"]

Manifest paths resolve relative to the manifest file. Theme ids must match [a-z0-9][a-z0-9-]{0,47}. Exactly one theme may set base = true; if none does, [theme_state].default selects the base. The base is emitted first even when it appears later in the manifest. Duplicate ids, duplicate labels, missing sources, invalid swatch colors, and unknown keys in strict mode are errors.

Build with:

casca scope-theme \
  --core dist/casca-core.css \
  --extension dist/casca-extended-charts.css \
  --extension dist/casca-extended-controls.css \
  --extension dist/casca-extended-layout.css \
  --theme-manifest site/casca-themes.toml \
  --output dist/casca.css

The compatibility CLI with --base, --overlay, and --picker-root remains available for direct bundle assembly.

Token ThemesLink to section

kind = "tokens" compiles a TOML token file into a .casca overlay. Required fields are [light] and [dark] gray ramps with 10 colors, surface_1, series with 8 colors, and on_accent. Role tables map gray indices to the semantic tokens:

[roles.light]
page_bg = "gray-0"
ink = "gray-9"
muted = "gray-7"
rule = "gray-3"
ink_inverse = "gray-0"
label_color = "gray-7"
axis_color = "gray-5"
grid_color = "gray-4"

The compiler emits --casca-gray-0 through --casca-gray-9, --casca-surface-1, role tokens, --casca-color-1 through --casca-color-8, chart label/axis/grid tokens, and --casca-on-accent. --casca-scatter-grid-color and --casca-radar-grid-color derive from grid_color unless scatter_grid_color or radar_grid_color is supplied in the role table.

Direct CSS ThemesLink to section

kind = "css" points at a source CSS overlay. The file must be rooted at .casca, may declare only --casca-* or --_ custom properties, and must not author :root, body, html, universal selectors, bare primitive roots, .casca-theme-picker, or body[data-casca-theme] roots. The scoper generates server-state and picker-preview roots.

Picker AccessibilityLink to section

The picker remains a native form with a fieldset, legend, labels, and radios. Keyboard users can Tab into the group and use arrow keys between radios; selection applies the mood preview immediately via CSS. Forced-colors mode uses system colors, and reduced motion removes the hover translate and checked glow.

Picker VariablesLink to section

The picker exposes these public variables:

Manifest-generated buttons set --_swatch-a and --_swatch-b inline. The built-in [data-mood] swatch rules remain as fallback styling.