Tweakit v0.7.2

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

TokenDrives
--twk-accentslider fills, focus rings, active highlights
--twk-on-accenttext drawn on the accent (active segment, radio label); the primary text tone when unset, so set it with a light accent
--twk-basepanel background, reused for recessed wells
--twk-dropdown-bgpopover / dropdown background
--twk-surface, -hover, -activecontrol surfaces and their interaction steps
--twk-border, --twk-border-hoverhairline borders
--twk-selectiontext-selection wash
--twk-text-root, -section, -primary, -label, -secondary, -tertiary, -focusthe text hierarchy: title, section, values, labels, muted, faint, focus
--twk-success, --twk-dangercopy-confirmation and invalid-input accents
--twk-shadow-dropdown, --twk-shadow-panel, --twk-shadow-panel-liftedpopover, panel and floating elevations
--twk-font-sans, --twk-font-monofont stacks
--twk-radius, --twk-row-heightcorner radius and row height
--twk-widththe 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);
}