Setup
Install
npm install @domphy/core @domphy/theme @domphy/ui@domphy/core and @domphy/theme are peer dependencies of @domphy/ui — all three must be installed. If you only need theme tokens without the UI patches:
npm install @domphy/core @domphy/themeApply Theme CSS
Call themeApply() once on the client. It injects a <style id="domphy-themes"> tag into <head>.
import { themeApply } from "@domphy/theme"
themeApply()If you need to control the target style element, pass one explicitly:
const styleTag = document.createElement("style")
themeApply(styleTag)That is mainly useful for Shadow DOM or isolated preview roots.
Choose The Active Theme
light is the default — themeApply() emits it on :root as well as [data-theme="light"], so a page that sets nothing renders the light theme rather than an unstyled one. Set dataTheme on any root element to pick a different theme (or to scope one to a subtree).
{ div: [App], dataTheme: "light" }
{ div: [App], dataTheme: "dark" }light and dark are built in. dark is derived from light — each ramp reversed, each baseTones index mirrored, direction: "lighten" — and that derivation is re-run on every setTheme("light", …), so a brand palette applied to light shows up in dark automatically:
setTheme("light", generateTheme({ primary: "#ff6600" }))
// getTheme("dark").colors.primary is now the reversed orange ramp — not the stock blueAnything you set explicitly on dark is replayed on top of each rebuild, in any call order, so dark-only overrides are never lost:
setTheme("dark", { colors: { primary: myHandTunedDarkRamp } })
setTheme("light", generateTheme({ primary: "#ff6600" }))
// dark.primary is still myHandTunedDarkRamp; every other role followed lightdataTheme can appear at any nesting level. Descendants inside that subtree resolve colors from the nearest theme root.
Register A Custom Theme
Use setTheme() to register or override a theme. All fields are optional — unspecified ones inherit from light.
Replace a built-in color family
Override an existing family (e.g. swap primary to a violet ramp):
import { setTheme, themeApply } from "@domphy/theme"
setTheme("brand", {
colors: {
primary: ["#ffffff", "#f7f5ff", "#efe8ff", "#e5d9ff", "#d6c2ff", "#c4a6ff", "#af87ff", "#9a6dff", "#8658ff", "#7345f7", "#6033df", "#512bc0", "#43249e", "#351c7d", "#28155d", "#1c0e3f", "#0e0720", "#000000"],
},
baseTones: {
primary: 9,
},
})
themeApply()Add a brand-new color family
A new colors key is valid on any theme name, but themeColor() does not pick it up from "brand" alone. themeVars() always reads getTheme("light") and emits the shared var(--…) baseline; themeColor throws unless the role is also registered on "light".
import { setTheme, themeApply, themeColor } from "@domphy/theme"
const gold = [
"#fffef0", // 0 — near-white
"#fffbd0",
"#fff6a0",
"#ffee65",
"#ffe030",
"#f5cc00",
"#d9b200",
"#bf9a00",
"#a58200",
"#8a6b00", // 9 — accent zone
"#705600",
"#574200",
"#3f2f00",
"#2d1f00",
"#1f1500",
"#130d00",
"#070400",
"#000000", // 17 — black
]
// Required: the light structure is what themeVars()/themeColor() key on.
setTheme("light", {
colors: { "brand-gold": gold },
baseTones: { "brand-gold": 9 },
})
// Optional named theme — starts as a clone of light, then deep-merges.
setTheme("brand", {
colors: { "brand-gold": gold },
baseTones: { "brand-gold": 9 },
})
themeApply()
const GoldBadge = {
span: "New",
style: {
background: (l) => themeColor(l, "shift-2", "brand-gold"),
color: (l) => themeColor(l, "shift-11", "brand-gold"),
},
}Ramps must be 18 steps (TONE_STEPS). On a direction: "darken" theme, index 0 is lightest and 17 is darkest.
Registering the family on "light" is enough for dark mode too: setTheme("light", …) re-runs the light → dark derivation, so "brand-gold" lands on "dark" as [...gold].reverse() with baseTones mirrored to 17 - 9. Override it only if you want a different dark ramp than the reverse:
setTheme("dark", {
colors: { "brand-gold": myHandTunedDarkGold },
baseTones: { "brand-gold": 8 },
})Custom theme names are not derived — setTheme creates no dark sibling for "brand". Register "brand-dark" yourself if you need one.
Then activate a named theme when you have one:
{ div: [App], dataTheme: "brand" }Custom design tokens
Arbitrary key-value tokens can be stored under custom. They appear as --custom-{key} CSS variables:
setTheme("light", {
custom: {
"border-radius-pill": "9999px",
"sidebar-width": "240px",
"topbar-height": "56px",
},
})
// themeVars() reads getTheme("light") — keys registered only on another
// theme name do not appear here.
import { themeVars } from "@domphy/theme"
const vars = themeVars()
// vars.custom["border-radius-pill"] === "var(--custom-border-radius-pill)"SSR
For SSR, inline themeCSS() on the server.
import { themeCSS } from "@domphy/theme"
const html = `<!DOCTYPE html>
<html>
<head>
<style id="domphy-themes">${themeCSS()}</style>
</head>
<body>
<div data-theme="light">...</div>
</body>
</html>`If the CSS is already in the HTML, the client usually does not need to call themeApply() again unless you later change registered themes.
For the full API surface, see API.