Theming
The kit's entire appearance runs on --tw-* custom properties; a theme is
just a bag of overrides. Every key is optional — a partial theme moves only what it
names, and everything else keeps the default monochrome look.
Light & dark
The kit ships both looks. Dark is the default, and panels follow the OS
to light on their own (prefers-color-scheme) — like this whole site
does. To pin a subtree, set data-tw-scheme="light" or
"dark" on any ancestor: forcing beats the OS preference, and portaled
popovers carry the scheme with them. Flip the segmented control — the panel
themes itself, whatever your system is set to.
const panel = tweaks("Scheme", {
scheme: { type: "segmented", options: ["Auto", "Light", "Dark"], value: "Auto" },
glow: [24, 0, 80, 1],
tint: "#7C5CFF",
live: true,
});
mount.append(panel.el);
panel.on((p) => {
if (p.scheme === "Auto") delete mount.dataset.twScheme;
else mount.dataset.twScheme = p.scheme.toLowerCase();
});mount is the panel's slot inside the stage; target is the demo surface it controls. In your own page you'd just document.body.append(panel.el).
Theme at construction
Pass { theme } as the third argument. Friendly names cover
the common moves — accent is the big one (the default look is
deliberately accentless).
const panel = tweaks("Accented", {
warmth: [0.6, 0, 1, 0.05],
contrast: [1.1, 0.5, 2, 0.05],
enabled: true,
}, {
theme: { accent: "#7C5CFF", radius: 12 }, // bare numbers are px
});
mount.append(panel.el);Live retheming
panel.setTheme(theme) re-themes a mounted panel on the fly —
and setTheme(null) reverts to the default. Here one tweakit panel
themes another: the editor on the right drives the sample on the left.
const sample = tweaks("Sample", {
glow: [24, 0, 80, 1],
tint: "#7C5CFF",
mode: ["soft", "hard"],
on: true,
}, { draggable: false });
target.querySelector(".th-slot").append(sample.el);
const editor = tweaks("Theme", {
accent: "#7C5CFF",
base: "#242424",
radius: [8, 0, 24, 1],
density: [32, 24, 44, 1], // row height, px
back: { type: "button", label: "setTheme(null)", action: () => {
editor.reset(); // restore the editor's controls first…
sample.setTheme(null); // …then drop the override entirely
} },
});
mount.append(editor.el);
editor.on((t) => sample.setTheme({
accent: t.accent, base: t.base, radius: t.radius, density: t.density,
}));Every token
The full friendly-name surface (all optional, from the
Theme type):
| Token | Drives |
|---|---|
accent | slider fills, focus rings, active highlights |
base | panel background, reused for recessed wells |
dropdownBg | popover / dropdown background |
surface, surfaceHover, surfaceActive | control surfaces and their interaction steps |
border, borderHover | hairline borders |
selection | text-selection wash |
title, section, text, label, textMuted, textFaint, focus | the text hierarchy |
success | copy-confirmation accent |
danger | invalid-input accent |
shadow, shadowPanel, shadowPanelLifted | popover, panel and floating elevations |
font | font stack |
fontMono | monospace font stack |
radius, density | corner radius and row height — numbers are px |
The raw escape hatch
Any raw --tw-* key passes straight through as a custom
property (unknown bare names are ignored) — and because the whole kit renders
from --tw-* variables, plain page CSS works too: set them on any
ancestor and every panel inside inherits.
tweaks("Raw", schema, { theme: {
accent: "#39d353",
"--tw-ease-out": "ease-in-out", // any raw token rides along
} });
/* or, in plain CSS — no JS at all: */
.my-sidebar .tw-panel { --tw-accent: #39d353; --tw-radius: 4px; }Recipes
Three starting points beyond the default monochrome — each panel below is live, built with the theme object printed underneath.
const gallery = target.querySelector(".th-gallery");
// Dark-base recipes carry a full light text-tone set, so titles / labels / toolbar
// icons aren't dark-on-dark on a light page; the accent stays vivid because the
// active pill's label is auto-contrasted (--tw-on-accent). Every tone clears WCAG AA.
const recipes = {
Daylight: { base: "#f2f1ee", accent: "#5b4dff", dropdownBg: "#ffffff",
surface: "rgba(0, 0, 0, 0.05)", surfaceHover: "rgba(0, 0, 0, 0.09)",
border: "rgba(0, 0, 0, 0.08)", selection: "rgba(0, 0, 0, 0.15)",
title: "#1a1a1a", text: "rgba(0, 0, 0, 0.85)", section: "rgba(0, 0, 0, 0.6)",
label: "rgba(0, 0, 0, 0.6)", textMuted: "rgba(0, 0, 0, 0.58)", textFaint: "rgba(0, 0, 0, 0.46)" },
Terminal: { base: "#0d1117", accent: "#39d353", font: "ui-monospace, Menlo, monospace", radius: 4,
surface: "rgba(57, 211, 83, 0.07)", border: "rgba(57, 211, 83, 0.18)",
title: "#d6ffe0", text: "rgba(201, 255, 216, 0.92)", section: "rgba(201, 255, 216, 0.78)",
label: "rgba(201, 255, 216, 0.72)", textMuted: "rgba(201, 255, 216, 0.64)", textFaint: "rgba(201, 255, 216, 0.5)" },
Cozy: { base: "#241a14", accent: "#ff8a5b", radius: 16, density: 36,
surface: "rgba(255, 138, 91, 0.08)", border: "rgba(255, 138, 91, 0.14)",
title: "#fbeada", text: "rgba(250, 235, 222, 0.92)", section: "rgba(250, 235, 222, 0.76)",
label: "rgba(250, 235, 222, 0.7)", textMuted: "rgba(250, 235, 222, 0.62)", textFaint: "rgba(250, 235, 222, 0.5)" },
};
for (const [name, theme] of Object.entries(recipes)) {
const slot = document.createElement("div");
slot.className = "th-recipe";
gallery.append(slot);
const panel = tweaks(name, {
level: [60, 0, 100, 1],
mode: ["auto", "manual"],
on: true,
}, { theme, draggable: false });
slot.append(panel.el);
}