Theming (Tailwind v4)
QuestKit ships its design tokens as a Tailwind v4 @theme block. Every component reads from --color-qk-*, --radius-qk, and --font-qk — override the variables, the whole library re-themes.
How @theme works
Tailwind v4's @theme directive registers a CSS variable and generates utility classes from it. Declaring --color-qk-primary makes bg-qk-primary, text-qk-primary, ring-qk-primary available everywhere — no tailwind.config.js needed.
Default tokens
These ship in @questkit/react/styles.css:
@theme {
--color-qk-primary: oklch(0.62 0.18 264); /* indigo */
--color-qk-bg: oklch(0.99 0.004 264); /* near-white */
--color-qk-fg: oklch(0.21 0.02 264); /* near-black */
--color-qk-coin: oklch(0.78 0.16 78); /* amber */
--color-qk-primary-hover: oklch(from var(--color-qk-primary) calc(l - 0.05) c h);
--color-qk-muted: oklch(from var(--color-qk-fg) calc(l + 0.4) c h);
--radius-qk: 0.75rem;
--font-qk: "Inter", ui-sans-serif, system-ui, sans-serif;
}
See the full variable table on Theming.
Why OKLCH?
OKLCH is the perceptually-uniform color space added in CSS Color Level 4. Two OKLCH colors with the same lightness look equally bright to the eye — unlike HSL, where hsl(60, 100%, 50%) (yellow) is dramatically brighter than hsl(240, 100%, 50%) (blue) despite identical "lightness". When you pick a palette, target a brightness rank:
bg: L 0.97–1.00 (near-white)fg: L 0.15–0.25 (deep, readable)primary: L 0.55–0.65 (vivid mid-tone — passes AA onbg)coin: L 0.70–0.80 (warm, brighter thanprimaryso it pops)
Overriding in your app
Add a second @theme block (or plain :root block) after importing the QuestKit styles. Last writer wins.
/* app.css */
@import "@questkit/react/styles.css";
@theme {
--color-qk-primary: oklch(0.62 0.20 30); /* coral */
--color-qk-coin: oklch(0.84 0.18 100); /* lemon */
--radius-qk: 0.25rem; /* squarer */
}
Dark mode
Combine @theme with a [data-theme="dark"] selector — the components read CSS variables, so the swap is automatic:
[data-theme="dark"] {
--color-qk-bg: oklch(0.18 0.01 264);
--color-qk-fg: oklch(0.95 0.01 264);
--color-qk-muted: oklch(0.40 0.02 264);
}
Toggle by setting document.documentElement.dataset.theme = "dark".
Campaign theme
Campaigns can carry a CampaignTheme.primaryColor that overrides --color-qk-primary for that campaign's banner and any nested mission cards. Recipe:
const { data } = useCampaign("spring-2026");
return (
<div style={{ "--color-qk-primary": data?.campaign.theme?.primaryColor }}>
<CampaignBanner campaignId="spring-2026" />
{/* nested mission cards inherit the override */}
</div>
);
Embed (Shadow DOM) considerations
The vanilla embed mounts each widget inside an open Shadow DOM. Host-page CSS variables that live on :root do cascade into the shadow root, so theming via --color-qk-* works as expected. Your host CSS classes and utility classes don't leak in — that's intentional (the embed is designed to look the same on any page).