Docs/Palette & Theming

Palette & Theming

Palette

@katforge/spark defines a single semantic color system that every component reads. The red on a flash error, a field error border, a SparkIcon error tone, and a destructive button all resolve to the same --color-kf-error, so cohesion is a consequence of the model, not a review check.

Brand
--color-kf-primary
Brand accent. Tracks the consumer palette; ember #FF8C00 by default.
--color-kf-primary-light
Hover/lighter variant.
--color-kf-primary-hover
Deeper variant for solid-button hover.
Surface
--color-kf-surface
Page background. Darkest tier.
--color-kf-surface-raised
Cards, modals, menus — the "above the page" tier.
--color-kf-surface-sunken
Inputs, wells — the "below the page" tier.
Text
--color-kf-text
Body copy.
--color-kf-text-secondary
Labels, metadata.
--color-kf-text-muted
Placeholders, disabled state.
Feedback
--color-kf-success
Saved. Completed. Green.
--color-kf-warning
Caution. Non-blocking. Amber.
--color-kf-error
Failure. Destructive. Red.
--color-kf-info
Informational. Blue.
--color-kf-neutral
Non-semantic chrome. Gray.

Tokens fall into four groups:

GroupPurpose
Brandprimary, primary-light, primary-hover — the ember accent. Tracks the consumer palette (see Brand Palettes), so on Stumper these become violet, on Lextris cyan, etc.
Surfacesurface, surface-raised, surface-sunken — three tiers (page / cards / wells). Also palette-aware.
Texttext, text-secondary, text-muted — the content hierarchy. Also palette-aware.
Feedbacksuccess, warning, error, info, neutralfixed across palettes. Red always means the same red everywhere, regardless of brand.

Every feedback color ships as both a hex (--color-kf-error) and an rgb tuple (--color-kf-error-rgb) so components can do alpha math against the canonical value:

CSS
/* soft tone backdrop anywhere in the system */
background: rgba(var(--color-kf-error-rgb), 0.15);

The brand tokens, surface tiers, and text hierarchy participate in the Brand Palettes system: they alias consumer-level variables (--color-primary, --color-surface-base, …) so a host site swaps them once and everything retints. Feedback colors deliberately don't participate. Green means success and red means error whether the brand is ember, arcanum, or anything else.

Theme CSS

Import the theme stylesheet to get the KATFORGE dark palette, orange accent, and component classes:

CSS
@import '@katforge/spark/theme.css';

The theme provides CSS custom properties and component classes under the kf- namespace. All interactive elements include :focus-visible indicators for keyboard accessibility.

Buttons
Cards
Raised
Default raised panel
Glass
Frosted backdrop blur
Spinners
Primary
Light

Buttons

HTML
<button class="kf-btn">Primary</button>
<button class="kf-btn" disabled>Disabled</button>
<button class="kf-btn-ghost">Ghost</button>

Inputs

HTML
<label class="kf-label">Email</label>
<input class="kf-input" placeholder="you@example.com" />

<!-- Invalid state via data attribute -->
<input class="kf-input" data-invalid placeholder="Invalid" />

Cards

Two card variants for different contexts:

HTML
<!-- Default raised card -->
<div class="kf-card">Content</div>

<!-- Frosted glass with backdrop blur -->
<div class="kf-card-glass">Content</div>

For a brand-neutral cool surface (the look formerly provided by kf-card-stone), pass palette="palettes.stone" to SparkModal. See Brand Palettes below.

Spinners

Use the kf-spinner class for a pure-CSS loading indicator. Explicit dimensions are required.

HTML
<!-- Brand-colored spinner -->
<span class="kf-spinner w-5 h-5" />

<!-- White spinner (for use on colored backgrounds) -->
<span class="kf-spinner kf-spinner-light w-5 h-5" />

For the Vue component with a size prop and built-in ARIA attributes, see SparkSpinner.

Design Tokens

The theme exposes CSS custom properties you can reference directly. Most color tokens alias consumer-level variables (see Brand Palettes) so Spark components follow whatever palette the host app sets; values shown are the ember fallback used when no consumer var is defined.

Brand colors (override these to re-skin)

CSS
var(--color-kf-primary)         /* → --color-primary,        fallback #FF8C00 */
var(--color-kf-primary-rgb)     /* → --color-primary-rgb,    fallback 255, 140, 0 */
var(--color-kf-surface)         /* → --color-surface-base,   fallback #140C08 */
var(--color-kf-surface-raised)  /* → --color-surface-raised, fallback #1E1410 */
var(--color-kf-surface-sunken)  /* → --color-surface-sunken, fallback #0E0806 */
var(--color-kf-text)            /* → --color-text-primary,   fallback #FFF0E0 */
var(--color-kf-text-secondary)  /* → --color-text-secondary, fallback #C0A080 */
var(--color-kf-text-muted)      /* → --color-text-muted,     fallback #705838 */

Derived & shared colors (do not override)

CSS
/* Derived from the aliased primary-rgb */
var(--color-kf-border)          /* rgba(<primary-rgb>, 0.2) */
var(--color-kf-border-strong)   /* rgba(<primary-rgb>, 0.4) */
var(--color-kf-hover)           /* rgba(<primary-rgb>, 0.1) */
var(--color-kf-focus)           /* rgba(<primary-rgb>, 0.4) */

/* Fixed across palettes */
var(--color-kf-primary-hover)   /* #E67E00 */
var(--color-kf-border-subtle)   /* rgba(255, 255, 255, 0.1) */
var(--color-kf-error)           /* #EF4444 */
var(--color-kf-success)         /* #22C55E */

Currency, radii, and effects

CSS
/* Currency — distinct hues so warm earned and cool paid never read the same. Override either per realm. */
var(--color-kf-ember)           /* #FB923C — warm orange, used by SparkEmberBalance */
var(--color-kf-ember-rgb)       /* 251, 146, 60 */
var(--color-kf-ingot)           /* #F5C046 — cool gold, used by SparkIngotBalance */
var(--color-kf-ingot-rgb)       /* 245, 192, 70 */

/* Radii */
var(--radius-kf-sm)  /* 6px  — small elements */
var(--radius-kf-md)  /* 8px  — buttons, inputs */
var(--radius-kf-lg)  /* 16px — cards, modals */

/* Effects — derived from the aliased primary-rgb */
var(--shadow-kf-glow)   /* colored glow on hover */
var(--shadow-kf-focus)  /* focus ring */

Brand Palettes

Where the Palette section above covers the semantic token model (one error red everywhere, three surface tiers, etc.), this section covers how the brand-scoped tokens (primary, surfaces, text) get re-skinned per site.

@katforge/spark ships two canonical brand palettes as typed data. Consumer apps can:

  • Inherit one of spark's palettes by setting a handful of consumer-level CSS variables.
  • Define their own game-specific palette with the same shape.
  • Override per-modal via SparkModal's palette prop, scoping the override to that modal subtree.

Canonical palettes

IDPurpose
emberThe KATFORGE default. Warm dark brown surfaces, orange primary. Used when no consumer vars are set.
stoneBrand-neutral cool surface. Used by SparkLogin to keep the auth experience consistent across realms.

Every --color-kf-* variable in Spark's theme aliases a consumer variable with an ember fallback:

CSS
--color-kf-primary:         var(--color-primary,        #FF8C00);
--color-kf-surface-raised:  var(--color-surface-raised, #1E1410);
--color-kf-text:            var(--color-text-primary,   #FFF0E0);

If your app sets --color-primary on :root, every Spark component (buttons, borders, modal card, embers) tracks it automatically. If your app sets nothing, Spark renders as KATFORGE ember. You rarely need to read these alias chains directly — paletteStyle() does the wiring.

Apply a spark palette site-wide

Imperatively, at app boot:

TypeScript
import { palettes, paletteStyle } from '@katforge/spark';

for (const [ key, value ] of Object.entries (paletteStyle (palettes.ember))) {
   document.documentElement.style.setProperty (key, value);
}

Or declaratively in CSS:

CSS
:root {
   --color-primary:          #FF8C00;
   --color-primary-rgb:      255, 140, 0;
   --color-surface-base:     #140C08;
   --color-surface-raised:   #1E1410;
   --color-surface-sunken:   #0E0806;
   --color-text-primary:     #FFF0E0;
   --color-text-secondary:   #C0A080;
   --color-text-muted:       #705838;
}

Defining a game-specific palette

Game apps typically declare their brand once in static CSS. No runtime switcher, no store: the single palette is the game's identity.

CSS
:root {
   /* Stumper — Arcanum */
   --color-primary:          #8B5CF6;
   --color-primary-rgb:      139, 92, 246;
   --color-surface-base:     #0C0C14;
   --color-surface-raised:   #14142A;
   --color-surface-sunken:   #08080E;
   --color-text-primary:     #F0F0FF;
   --color-text-secondary:   #A0A0C0;
   --color-text-muted:       #505070;
}

If you need the palette as a runtime value (e.g. to pass to SparkModal), declare it with the exported SparkPalette type:

TypeScript
import type { SparkPalette } from '@katforge/spark';

export const arcanum: SparkPalette = {
   id: 'arcanum',
   label: 'Arcanum',
   primary: '#8B5CF6',
   primaryRgb: '139, 92, 246',
   surface: { base: '#0C0C14', raised: '#14142A', sunken: '#08080E' },
   text:    { primary: '#F0F0FF', secondary: '#A0A0C0', muted: '#505070' },
};

Per-modal palette overrides

Pass a palette prop to SparkModal to pin a single modal's look. The palette's CSS vars are scoped to the modal subtree, so card surface, borders, and embers all recolor together without affecting the rest of the page.

Vue
<template>
   <SparkModal
      :model-value="open"
      :palette="palettes.stone"
      @update:model-value="open = $event"
   >
      <p>This modal stays brand-neutral regardless of the host's palette.</p>
   </SparkModal>
</template>

<script setup>
import { ref } from 'vue';
import { SparkModal, palettes } from '@katforge/spark';

const open = ref (false);
</script>

Why the login modal ignores the host palette

SparkLogin renders SparkModal internally with :palette="palettes.stone". Because palette vars are set inline on the modal's scope, they win over any --color-* variable the host set at the :root level. The login surface always renders with a cool stone background and the KATFORGE brand-orange embers, even on sites with a different primary.

Use the same technique for any modal that must stay on-brand rather than on-theme (settings in a co-branded context, legal disclaimers, multi-realm dashboards, etc.).