Tweakit

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):

TokenDrives
accentslider fills, focus rings, active highlights
basepanel background, reused for recessed wells
dropdownBgpopover / dropdown background
surface, surfaceHover, surfaceActivecontrol surfaces and their interaction steps
border, borderHoverhairline borders
selectiontext-selection wash
title, section, text, label, textMuted, textFaint, focusthe text hierarchy
successcopy-confirmation accent
dangerinvalid-input accent
shadow, shadowPanel, shadowPanelLiftedpopover, panel and floating elevations
fontfont stack
fontMonomonospace font stack
radius, densitycorner 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);
}