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.
Tokens fall into four groups:
| Group | Purpose |
|---|---|
| Brand | primary, primary-light, primary-hover — the ember accent. Tracks the consumer palette (see Brand Palettes), so on Stumper these become violet, on Lextris cyan, etc. |
| Surface | surface, surface-raised, surface-sunken — three tiers (page / cards / wells). Also palette-aware. |
| Text | text, text-secondary, text-muted — the content hierarchy. Also palette-aware. |
| Feedback | success, warning, error, info, neutral — fixed 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:
/* 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:
@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
<button class="kf-btn">Primary</button>
<button class="kf-btn" disabled>Disabled</button>
<button class="kf-btn-ghost">Ghost</button>
Inputs
<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:
<!-- 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.
<!-- 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)
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)
/* 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
/* 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
paletteprop, scoping the override to that modal subtree.
Canonical palettes
| ID | Purpose |
|---|---|
ember | The |
stone | Brand-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:
--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:
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:
: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.
: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:
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.
<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.).