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.

PropTypeDefaultDescription
colorValueOrState<ThemeColor>"neutral"Resting text and hover background tone.
accentColorValueOrState<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:

  1. Patch props. Each patch exposes a small, stable set of props—typically fewer than five. Lowest friction.
  2. Context attributes. Use dataTone, dataSize, and dataDensity on a container to shift tone, size, or density for an entire subtree without touching individual elements.
  3. Inline override. Native-wins merge strategy: any property set directly on the element overrides the patch value.
  4. 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:

Uw=0w=1w=2w=3
height (n = 1)691215
paddingBlock01.534.5
paddingInline34.564.5
radius01.534.5

Tone - K = N / 2 where N is the palette length. For N = 18, K = 9.

RoleShiftn=0
Backgroundparent +/- n0
Textbg + K6
Borderbg + K/23
Hoverbg + 2K/34
Selected / Focusabove +/- K/32-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>