Segmented Item
Use segmentedItem on a <button> placed inside a segmented control. It reads the parent segmented context, sets aria-selected, and handles click-to-select. Use _key on each button to set its selection key; otherwise the index is used.
| Prop | Type | Default | Description |
|---|---|---|---|
color | ValueOrState<ThemeColor> | "neutral" | Resting text and hover background tone. |
accentColor | ValueOrState<ThemeColor> | "primary" | Selected state background and focus-outline tone. |
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 ElementNode,
type PartialElement,
toState,
type ValueOrState,
} from "@domphy/core";
import {
type ThemeColor,
themeColor,
themeSize,
themeSpacing,
} from "@domphy/theme";
/**
* Styles and wires a single option inside a `segmented` control on the host `<button>`.
* Sets `aria-selected` and handles click-to-select against the parent `segmented` context.
*
* @hostTag button
* @param props.color - Theme color for resting state. Defaults to `"neutral"`.
* @param props.accentColor - Theme color for selected state. Defaults to `"primary"`.
* @example { button: "Month", $: [segmentedItem()] }
*/
function segmentedItem(
props: {
color?: ValueOrState<ThemeColor>;
accentColor?: ValueOrState<ThemeColor>;
} = {},
): PartialElement {
const color = toState(props.color ?? "neutral", "color");
const accentColor = toState(props.accentColor ?? "primary", "accentColor");
return {
role: "option",
_onInsert: (node) => {
if (node.tagName !== "button") {
console.warn(`"segmentedItem" patch must use button tag`);
}
const ctx = node.getContext("segmented");
if (!ctx) {
console.warn(`"segmentedItem" patch must be used inside a "segmented"`);
return;
}
const siblings = (node.parent?.children.items ?? []) as ElementNode[];
const items = siblings.filter(
(sibling) =>
sibling.type === "ElementNode" &&
sibling.attributes.get("role") === "option",
);
// 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(
"ariaSelected",
(listener) => ctx.value.get(listener) === key,
);
node.addEvent("click", () => ctx.value.set(key));
},
style: {
cursor: "pointer",
fontSize: (listener) => themeSize(listener, "inherit"),
height: themeSpacing(6),
paddingBlock: themeSpacing(1),
paddingInline: themeSpacing(3),
border: "none",
borderRadius: themeSpacing(10),
color: (listener) => themeColor(listener, "shift-9", color.get(listener)),
backgroundColor: "transparent",
transition: "background-color 300ms ease",
"&:hover:not([disabled]):not([aria-selected=true])": {
backgroundColor: (listener) =>
themeColor(listener, "shift-3", color.get(listener)),
},
"&[aria-selected=true]": {
backgroundColor: (listener) =>
themeColor(listener, "shift-0", accentColor.get(listener)),
color: (listener) =>
themeColor(listener, "shift-10", 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 { segmentedItem };
</div>
<div class="block" data-tab="1">
import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import { type ThemeColor, themeColor, themeSpacing } from "@domphy/theme";
/**
* Container patch that establishes a `segmented` context for single-select navigation.
* Style: inline pill-shaped control with muted background. Use with `segmentedItem` patches on child `<button>` elements.
*
* @param props.value - Initially selected item key. Accepts a value or state. Defaults to `""`.
* @param props.color - Theme color for the control background. Defaults to `"neutral"`.
* @example { div: null, $: [segmented({ value: "month" })] }
*/
function segmented(
props: { value?: ValueOrState<string>; color?: ThemeColor } = {},
): PartialElement {
const { color = "neutral" } = props;
return {
role: "group",
_context: {
segmented: { value: toState(props.value ?? "") },
},
style: {
display: "inline-flex",
paddingBlock: themeSpacing(1),
paddingInline: themeSpacing(1),
gap: themeSpacing(0.5),
borderRadius: themeSpacing(10),
backgroundColor: (listener) => themeColor(listener, "shift-2", color),
},
};
}
export { segmented };
</div>
</div>