05customization

Theming

Nineteen themes across three families, light and dark schemes inside every one, and a zero-flash SSR restore script.

Nineteen themes, each shipping light and dark token blocks, all driven from the @/components/kloq/lib/themes module. A theme is a set of coss token overrides in CSS; this module is the metadata and the apply/persist side.

How a theme works

Each theme is a [data-kloq-theme="<id>"] block in the stylesheet that re-points the coss custom properties (--background, --primary, --radius, --shadow-lg, …) for both light and dark schemes — so the light/dark toggle keeps working inside any theme. setTheme stamps the attribute on <html>, persists to localStorage, and (motion permitting) wraps the swap in a ~200ms crossfade so the reveal reads as one deliberate transition instead of a hard repaint.

import { THEMES, setTheme, setScheme } from "@/components/kloq/lib/themes";

setTheme("midnight"); // stamps <html data-kloq-theme="midnight">, persists
setScheme("dark");    // "light" | "dark" | "system"

The default theme is "coss" — picking it removes the attribute and the stored key rather than setting them, so an untouched install carries no state at all.

The three families

Seven coss themes are authored directly against the coss token palette: Coss, Midnight, Paper, Forest, Ultraviolet, Terminal and Rosé. Seven editor themes are ports of well-known code-editor palettes — Dracula, Nord, Tokyo Night, Catppuccin, Gruvbox, Solarized and One. Each editor port uses a real upstream light and dark palette (Catppuccin Mocha / Latte, One Dark / One Light — no invented counterparts), mapped onto coss's semantic tokens. Every entry in THEMES carries its metadata: an id, a display name, a three-color swatch for picker chips, its family, the scheme it was originally designed in (origin), and an upstream credit.

Five saas themes rebuild the look of well-known products from their public CSS — Cal.com under its own name, and Linear, Raycast, Vercel and ChatGPT as Graphite, Ember, Geist and Ivory. Radius, border alpha, shadow stacks and type were measured, not guessed. These are the ones that exercise the shadow tokens: every theme sets --shadow-xs / --shadow-sm / --shadow-md / --shadow-lg (built on --shadow-color), and every elevated surface — popovers, menus, dialogs, cards, form controls, the drag lift — reads them at runtime. Geist picks up --font-geist-sans and Cal.com picks up --font-cal-sans when the host defines them.

import { THEMES, type KloqTheme } from "@/components/kloq/lib/themes";

THEMES.map((t) => t.id);
// "coss" | "midnight" | "paper" | "forest" | "ultraviolet" | "terminal"
// | "rose" | "dracula" | "nord" | "tokyo-night" | "catppuccin"
// | "gruvbox" | "solarized" | "one"
// | "calcom" | "graphite" | "ember" | "geist" | "ivory"

Light, dark, and system

The scheme is a second, independent axis. .dark on <html> is the coss convention — a host that already drives that class can ignore kloq's scheme API entirely; the module only touches the class once setScheme is called. "system" clears the stored preference and follows the OS from then on; watchSystemScheme keeps it live across OS changes and returns an unsubscribe.

import {
  getScheme,      // the stored preference — may be "system"
  resolveScheme,  // what's on screen — "system" collapsed to light/dark
  toggleScheme,   // light ⇄ dark, resolving "system" first
  watchSystemScheme,
} from "@/components/kloq/lib/themes";

const off = watchSystemScheme(); // re-applies on OS changes while pref is "system"

Scoping a theme to one calendar

setTheme and setScheme are page-level: they stamp <html> and persist, and every calendar on the page follows. To dress a single instance instead — two calendars in two brands, a themed embed inside a host with its own look, a theme picker that previews without committing — pass theme and scheme as props. They stamp the calendar's root and its portal container (menus, popovers and dialogs render at body level, so the scope has to travel with them) and touch nothing else: no <html> attribute, no localStorage.

<Kloq theme="dracula" scheme="dark" />
<Kloq theme="solarized" scheme="light" />

Omit either prop and that axis follows the page again. scheme pins the colour scheme even when the page's .dark disagrees — a scheme="light" calendar stays light on a dark page, dark: utilities included.

Zero-flash SSR restore

Theme and scheme persist in localStorage, which the server cannot read — so a server-rendered page would flash the default theme before hydration. THEME_RESTORE_SCRIPT is a tiny inline script that restores both axes before first paint; without the scheme half, a dark preference flashes light on every load.

app/layout.tsx
import { THEME_RESTORE_SCRIPT } from "@/components/kloq/lib/themes";

// in your document <head>, before first paint:
<script dangerouslySetInnerHTML={{ __html: THEME_RESTORE_SCRIPT }} />

The full surface

ExportTypeNotes
THEMESKloqTheme[]All nineteen themes with id, name, swatch, family, origin and credit.
setTheme(id)(id: string) => voidStamps data-kloq-theme, persists, crossfades. "coss" clears both.
getTheme()() => stringThe active theme id; "coss" on the server or when unset.
setScheme(pref)(pref: KloqScheme) => voidApplies and persists "light" | "dark" | "system".
getScheme()() => KloqSchemeThe stored preference — may be "system".
resolveScheme(pref?)() => "light" | "dark"What's actually on screen, "system" collapsed.
toggleScheme()() => "light" | "dark"Flip light ⇄ dark; returns what it landed on.
watchSystemScheme()() => () => voidFollow OS changes while the preference is "system". Returns unsubscribe.
THEME_RESTORE_SCRIPTstringThe pre-paint inline restore script for SSR.
THEME_STORAGE_KEY / SCHEME_STORAGE_KEYstringThe localStorage keys (kloq-theme / kloq-scheme), plus LEGACY_* twins.

Reduced motion is honored throughout: with prefers-reduced-motion set, the crossfade is skipped and swaps apply instantly. Storage failures (private windows, blocked storage) are swallowed — the theme still applies for the session.

on this page