Toggle
Use the toggle patch to customize this element.
Customization
Must see the source of patch at the bottom of each patch page to understand the structure then code it still code as html native element.
There are four levels of customization, in increasing order of effort:
- Patch props. Each patch exposes a small, stable set of props—typically fewer than five. Lowest friction.
- Context attributes. Use
dataTone,dataSize, anddataDensityon a container to shift tone, size, or density for an entire subtree without touching individual elements. - Inline override. Native-wins merge strategy: any property set directly on the element overrides the patch value.
- Create a variant. Clone a similar patch and edit it. Use this only when you need a reusable custom version.
Formulas
Unit - U = fontSize / 4 - convert final values with themeSpacing(n).
Size - n = intrinsic text lines, w = wrapping level, d = density factor:
height = (n * 6 + 2 * d * w) * U
paddingBlock = d * w * U
paddingInline = ceil(3 / w) * d * w * U
radius = d * w * U
Base density d = 1.5:
| U | w=0 | w=1 | w=2 | w=3 |
|---|---|---|---|---|
height (n = 1) | 6 | 9 | 12 | 15 |
| paddingBlock | 0 | 1.5 | 3 | 4.5 |
| paddingInline | 3 | 4.5 | 6 | 4.5 |
| radius | 0 | 1.5 | 3 | 4.5 |
Tone - K = N / 2 where N is the palette length. For N = 18, K = 9.
| Role | Shift | n=0 |
|---|---|---|
| Background | parent +/- n | 0 |
| Text | bg + K | 6 |
| Border | bg + K/2 | 3 |
| Hover | bg + 2K/3 | 4 |
| Selected / Focus | above +/- K/3 | 2-4 |
State shift range: K/3 <= delta <= 2K/3.
<div class="blocks">
<div class="block active" data-tab="0">
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
type ThemeColor,
themeColor,
themeSize,
themeSpacing,
} from "@domphy/theme";
/**
* Container patch that establishes a `toggleGroup` context (shared selection
* `value` + `multiple` flag) and `group` role for child `toggle` patches, with
* a bordered segmented-control style. No host tag check; typically applied to a
* wrapper element.
*
* @param props.value - Selected toggle key(s). Optional, accepts a value or state of `string | string[]`. Defaults to `[]` when `multiple`, otherwise `""`.
* @param props.multiple - When true, allows multiple toggles selected at once. Optional. Defaults to `false`.
* @param props.color - Theme color for the group background/border. Optional. Defaults to `"neutral"`.
* @example { div: null, $: [toggleGroup({ multiple: true })] }
*/
function toggleGroup(
props: {
value?: ValueOrState<string | string[]>;
multiple?: boolean;
color?: ThemeColor;
} = {},
): PartialElement {
const { multiple = false, color = "neutral" } = props;
return {
role: "group",
_context: {
toggleGroup: {
value: toState(props.value ?? (multiple ? [] : "")),
multiple,
},
},
style: {
display: "flex",
paddingBlock: themeSpacing(1),
paddingInline: themeSpacing(1),
gap: themeSpacing(1),
borderRadius: themeSpacing(2),
fontSize: (listener) => themeSize(listener, "inherit"),
backgroundColor: (listener) => themeColor(listener, "inherit", color),
outline: (listener) =>
`1px solid ${themeColor(listener, "shift-3", color)}`,
outlineOffset: "-1px",
},
};
}
export { toggleGroup };
</div>
<div class="block" data-tab="1">
import {
type ElementNode,
type PartialElement,
toState,
type ValueOrState,
} from "@domphy/core";
import {
type ThemeColor,
themeColor,
themeSize,
themeSpacing,
} from "@domphy/theme";
/**
* Styles a single toggle button inside a `toggleGroup` on the host `<button>`
* element. Wires up `aria-pressed` and click-to-toggle against the surrounding
* `toggleGroup` context (single- or multi-select). Must be used inside a
* `toggleGroup` patch.
*
* @hostTag button
* @param props.color - Theme color for the resting/hover background and text. Optional, accepts a value or state. Defaults to `"neutral"`.
* @param props.accentColor - Theme color for the pressed/focus state. Optional, accepts a value or state. Defaults to `"primary"`.
* @example { button: "Bold", $: [toggle()] }
*/
function toggle(
props: {
color?: ValueOrState<ThemeColor>;
accentColor?: ValueOrState<ThemeColor>;
} = {},
): PartialElement {
const color = toState(props.color ?? "neutral", "color");
const accentColor = toState(props.accentColor ?? "primary", "accentColor");
return {
role: "button",
_onInsert: (node) => {
if (node.tagName !== "button") {
console.warn(`"toggle" patch must use button tag`);
}
const ctx = node.getContext("toggleGroup");
if (!ctx) {
console.warn(`"toggle" patch must be used inside a "toggleGroup"`);
return;
}
const children = (node.parent?.children.items ?? []) as ElementNode[];
const items = children.filter(
(n) =>
n.type === "ElementNode" && n.attributes.get("role") === "button",
);
// node.key is null (not undefined) when absent — check both so an
// explicit _key of 0 or "" keeps its real key instead of "null"/index.
const key =
node.key !== null && node.key !== undefined
? String(node.key)
: String(items.indexOf(node));
node.attributes.set("ariaPressed", (listener) => {
const val = ctx.value.get(listener);
return Array.isArray(val) ? val.includes(key) : val === key;
});
node.addEvent("click", () => {
const val = ctx.value.get();
if (ctx.multiple) {
const arr = Array.isArray(val) ? [...val] : [];
ctx.value.set(
arr.includes(key) ? arr.filter((v) => v !== key) : [...arr, key],
);
} else {
ctx.value.set(val === key ? "" : key);
}
});
},
style: {
cursor: "pointer",
fontSize: (listener) => themeSize(listener, "inherit"),
height: themeSpacing(6),
paddingBlock: themeSpacing(1),
paddingInline: themeSpacing(2),
border: "none",
borderRadius: themeSpacing(1),
color: (listener) => themeColor(listener, "shift-9", color.get(listener)),
backgroundColor: (listener) =>
themeColor(listener, "inherit", color.get(listener)),
transition: "background-color 300ms ease",
"&:hover:not([disabled])": {
backgroundColor: (listener) =>
themeColor(listener, "shift-2", color.get(listener)),
},
"&[aria-pressed=true]": {
backgroundColor: (listener) =>
themeColor(listener, "shift-3", accentColor.get(listener)),
color: (listener) =>
themeColor(listener, "shift-12", accentColor.get(listener)),
},
"&:focus-visible": {
outline: (listener) =>
`${themeSpacing(0.5)} solid ${themeColor(listener, "shift-6", accentColor.get(listener))}`,
outlineOffset: `-${themeSpacing(0.5)}`,
},
"&[disabled]": {
opacity: 0.7,
cursor: "not-allowed",
},
},
};
}
export { toggle };
</div>
</div>