Skip to content
Domphy

Small

Styles small/secondary text: one step smaller font size (data-size="decrease-1") with a themed foreground color.

Contrast

The tone is shift-10, one step above the "text" floor, because this text renders below the WCAG large-text threshold and so must always clear 4.5. Measured in Chromium on the built bundle: 6.01–7.73 in light and 6.33–7.73 in dark on every edge-anchored surface (shift-0–shift-3, shift-14–shift-17).

Three things forfeit that guarantee, and all three belong to the call site:

  • A host-declared style.color wins over the patch (native beats patch). "text" / shift-9 on a shift-1 surface measures 4.23 — an axe color-contrast failure at this size. Pass color for a different family instead of restyling.
  • An ancestor's scoped & small rule wins too, even when the call site did everything right. A descendant selector is specificity (0,1,1); this patch's own generated class is (0,1,0), so it loses the cascade:

    // inside a correctly tone-anchored card — dataTone: "shift-1", background "inherit"
    "& small": {
      display: "block",
      color: (l) => themeColor(l, "shift-8"), // ← silently outranks small()
    },

    Measured with no inline style on the <small> at all: that shift-8 emits var(--neutral-9) = #707070 on the card's #ededed = 4.23. Delete the color line and the same element measures 6.27 (light) / 7.21 (dark). Scope layout in such a rule and leave the colour to the patch.

    The specificity is deliberately not escalated to win this: a patch that outranked descendant rules would leave an author no way to restyle short of !important.

  • A surface tinted without a tone context — backgroundColor: (l) => themeColor(l, "shift-2") instead of dataTone: "shift-2" + themeColor(l, "inherit"). Every child then still resolves against the page root, and even shift-10 drops to 4.34 on a shift-2 tint and 3.67 on shift-3 (light). @domphy/doctor's tone-background-inherit rule flags exactly this.

Props

PropTypeDefaultDescription
colorValueOrState<ThemeColor>"neutral"Tone for the text.
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.

import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import { type ThemeColor, themeColor, themeSize } from "@domphy/theme";

/**
 * Styles small/secondary text: one step smaller font size (`data-size="decrease-1"`) with a
 * themed foreground color.
 *
 * The tone is `shift-10`, one step ABOVE the `"text"` floor, because this text renders below
 * the WCAG large-text threshold and so must always clear 4.5:1. Measured in Chromium on the
 * built bundle: 6.01–7.73:1 (light) and 6.33–7.73:1 (dark) on every edge-anchored surface.
 * Three things at the CALL SITE forfeit that guarantee, all measured in Chromium:
 * 1. A host-declared `style.color` wins over the patch (native beats patch) — `"text"` /
 *    `shift-9` on a `shift-1` surface is 4.23:1. Pass `color` for a different family
 *    instead of restyling.
 * 2. An ancestor's scoped `"& small": { color: … }` also wins: that descendant selector is
 *    specificity (0,1,1) and this patch's own generated class is only (0,1,0). Measured
 *    inside a correctly tone-anchored `dataTone: "shift-1"` card, with no inline style on
 *    the `<small>` at all: `& small { color: themeColor(l, "shift-8") }` resolves to
 *    `var(--neutral-9)` = #707070 on the card's #ededed = 4.23:1 (6.27:1 once the `color`
 *    line is dropped). The specificity is deliberately NOT escalated here — a patch that
 *    outranked descendant rules would leave an author no way to restyle short of
 *    `!important`. Scope layout there (`display: block`) and leave the colour to the patch.
 * 3. A surface tinted with a fixed `themeColor(l, "shift-N")` background and no `dataTone`:
 *    every child still resolves against the page root, and even `shift-10` drops to 4.34:1
 *    (shift-2 tint) and 3.67:1 (shift-3). Shift the surface with `dataTone` + `"inherit"`
 *    instead — `@domphy/doctor`'s `tone-background-inherit` rule flags exactly this.
 *
 * @hostTag small
 * @param props.color - Theme color tone for the text. Accepts a value or reactive state.
 *   Defaults to `"neutral"`.
 * @example { small: "fine print", $: [small()] }
 */
function small(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "neutral", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "small") {
        console.warn('"small" patch must use small tag');
      }
    },
    dataSize: "decrease-1",
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      // shift-10 (not muted/shift-8) keeps WCAG AA ≥4.5:1 on light surfaces for small type.
      color: (listener) =>
        themeColor(listener, "shift-10", color.get(listener)),
    },
  };
}

export { small };