Skip to main content

Theming

QuestKit's design tokens live in a single Tailwind v4 @theme block in @questkit/react/styles.css. Every component reads from these CSS variables — override them and the whole library re-themes. No tailwind.config.js, no runtime theme switcher dependency.

Token table

VariableDefaultDescriptionExample override
--color-qk-primaryoklch(0.62 0.18 264) (indigo)Main brand accent. Used for CTAs (<Claim> button), focus rings, <ProgressBar> fill, <CampaignBanner> countdown.oklch(0.62 0.20 30) (coral)
--color-qk-bgoklch(0.99 0.004 264) (near-white)Surface background. Component cards (<MissionCard>, <CampaignBanner>, <RewardClaimToast>) use this.oklch(0.18 0.01 264) (dark)
--color-qk-fgoklch(0.21 0.02 264) (near-black)Foreground / body text. Should pass AA contrast on --color-qk-bg.oklch(0.95 0.01 264) (light)
--color-qk-coinoklch(0.78 0.16 78) (amber)Gamification reward accent. <CoinBalance> number colour, mission reward badge background, toast accent badge.oklch(0.84 0.18 100) (lemon)
--color-qk-primary-hoveroklch(from var(--color-qk-primary) calc(l - 0.05) c h)Auto-derived darker primary for hover/active states.(usually leave as derived)
--color-qk-mutedoklch(from var(--color-qk-fg) calc(l + 0.4) c h)Auto-derived muted track / border colour. <ProgressBar> track, card borders, <MissionList> skeleton background.(usually leave as derived)
--radius-qk0.75remBorder-radius applied to cards, banner, buttons, badges, toasts.0.25rem (squarer)
--font-qk"Inter", ui-sans-serif, system-ui, ...Font stack used by every QuestKit widget."Roboto Mono", monospace

Overriding

Add a @theme (or plain :root) block after the QuestKit import:

/* app.css */
@import "@questkit/react/styles.css";

@theme {
--color-qk-primary: oklch(0.65 0.18 280);
--color-qk-coin: oklch(0.85 0.16 60);
--radius-qk: 0.25rem;
--font-qk: "JetBrains Mono", monospace;
}

Dark mode

[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.4 0.02 264);
}

Toggle by setting document.documentElement.dataset.theme = "dark". Components re-render visually on the next paint — no JS state coupling.

Campaign-scoped overrides

A Campaign.theme.primaryColor value (in the API response) is a CSS color you can apply via inline style for the duration of a campaign's UI:

const { data } = useCampaign("spring-2026");
const themeStyle = data?.campaign.theme?.primaryColor
? { ["--color-qk-primary" as string]: data.campaign.theme.primaryColor }
: undefined;

return (
<div style={themeStyle}>
<CampaignBanner campaignId="spring-2026" />
</div>
);

Reduced motion

QuestKit's stylesheet includes a global guard so animations short-circuit when the user prefers reduced motion:

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}

Individual components (<CoinBalance>'s rolling number, <SpinWheel>'s rotation, <ScratchCard>'s reveal) also branch internally to skip animation logic entirely under reduced motion — they don't just shorten the timing.