Theming
The kit's entire appearance runs on --twk-* 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-twk-scheme="light" or
"dark" on any ancestor: forcing beats the OS preference, and portaled
popovers carry the scheme with them. Flip the segmented control and the panel
themes itself, whatever your system is set to.
Each color token is a light-dark() pair picked by the panel's
color-scheme, which needs Chrome and Edge 123, Firefox 120 or Safari 17.5.
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.twkScheme;
else mount.dataset.twkScheme = p.scheme.toLowerCase();
});mount is the panel's slot inside the stage. In your own page you'd just document.body.append(panel.el).
Theme at construction
Pass { theme } as the third argument, keyed by token, each
value a CSS value; --twk-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: { "--twk-accent": "#7C5CFF", "--twk-radius": "12px" },
});
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: { type: "slider", value: 32, min: 24, max: 44, unit: "px" }, // row height
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({
"--twk-accent": t.accent, "--twk-base": t.base,
"--twk-radius": t.radius + "px", "--twk-row-height": t.density + "px",
}));Every token
The tokens a theme usually moves (all optional, from the
Theme type):
| Token | Drives |
|---|---|
--twk-accent | slider fills, focus rings, active highlights |
--twk-on-accent | text drawn on the accent (active segment, radio label); the primary text tone when unset, so set it with a light accent |
--twk-base | panel background, reused for recessed wells |
--twk-dropdown-bg | popover / dropdown background |
--twk-surface, -hover, -active | control surfaces and their interaction steps |
--twk-border, --twk-border-hover | hairline borders |
--twk-selection | text-selection wash |
--twk-text-root, -section, -primary, -label, -secondary, -tertiary, -focus | the text hierarchy: title, section, values, labels, muted, faint, focus |
--twk-success, --twk-danger | copy-confirmation and invalid-input accents |
--twk-shadow-dropdown, --twk-shadow-panel, --twk-shadow-panel-lifted | popover, panel and floating elevations |
--twk-font-sans, --twk-font-mono | font stacks |
--twk-radius, --twk-row-height | corner radius and row height |
--twk-width | the panel's width, 256px by default |
Any token, or plain CSS
Any --twk-* key passes straight through as a custom
property, the ones the table leaves out too; a key that isn't a token is skipped
with a console warning. And because the whole kit renders from these variables,
plain page CSS works as well: set them on .twk-panel, scoped by any ancestor.
A panel built with shadow: true takes them from its host instead: set them on
.twk-host, and its panes take them too.
tweaks("Raw", schema, { theme: {
"--twk-accent": "#39d353",
"--twk-ease-out": "ease-in-out", // any token rides along
} });
/* or, in plain CSS, no JS at all: */
.my-sidebar .twk-panel { --twk-accent: #39d353; --twk-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; each sets the text on its accent, so the
// active pill's label stays legible. Every tone clears WCAG AA.
const recipes = {
Daylight: { "--twk-base": "#f2f1ee", "--twk-accent": "#5b4dff", "--twk-on-accent": "#ffffff", "--twk-dropdown-bg": "#ffffff",
"--twk-surface": "rgba(0, 0, 0, 0.05)", "--twk-surface-hover": "rgba(0, 0, 0, 0.09)",
"--twk-border": "rgba(0, 0, 0, 0.08)", "--twk-selection": "rgba(0, 0, 0, 0.15)",
"--twk-text-root": "#1a1a1a", "--twk-text-primary": "rgba(0, 0, 0, 0.85)", "--twk-text-section": "rgba(0, 0, 0, 0.6)",
"--twk-text-label": "rgba(0, 0, 0, 0.6)", "--twk-text-secondary": "rgba(0, 0, 0, 0.58)", "--twk-text-tertiary": "rgba(0, 0, 0, 0.46)" },
Terminal: { "--twk-base": "#0d1117", "--twk-accent": "#39d353", "--twk-on-accent": "#161616",
"--twk-font-sans": "ui-monospace, Menlo, monospace", "--twk-radius": "4px",
"--twk-surface": "rgba(57, 211, 83, 0.07)", "--twk-border": "rgba(57, 211, 83, 0.18)",
"--twk-text-root": "#d6ffe0", "--twk-text-primary": "rgba(201, 255, 216, 0.92)", "--twk-text-section": "rgba(201, 255, 216, 0.78)",
"--twk-text-label": "rgba(201, 255, 216, 0.72)", "--twk-text-secondary": "rgba(201, 255, 216, 0.64)", "--twk-text-tertiary": "rgba(201, 255, 216, 0.5)" },
Cozy: { "--twk-base": "#241a14", "--twk-accent": "#ff8a5b", "--twk-on-accent": "#161616", "--twk-radius": "16px", "--twk-row-height": "36px",
"--twk-surface": "rgba(255, 138, 91, 0.08)", "--twk-border": "rgba(255, 138, 91, 0.14)",
"--twk-text-root": "#fbeada", "--twk-text-primary": "rgba(250, 235, 222, 0.92)", "--twk-text-section": "rgba(250, 235, 222, 0.76)",
"--twk-text-label": "rgba(250, 235, 222, 0.7)", "--twk-text-secondary": "rgba(250, 235, 222, 0.62)", "--twk-text-tertiary": "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);
}