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.colorwins over the patch (native beats patch)."text"/shift-9on ashift-1surface measures 4.23 — an axecolor-contrastfailure at this size. Passcolorfor a different family instead of restyling. An ancestor's scoped
& smallrule 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: thatshift-8emitsvar(--neutral-9)=#707070on the card's#ededed= 4.23. Delete thecolorline 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 ofdataTone: "shift-2"+themeColor(l, "inherit"). Every child then still resolves against the page root, and evenshift-10drops to 4.34 on ashift-2tint and 3.67 onshift-3(light).@domphy/doctor'stone-background-inheritrule flags exactly this.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
color | ValueOrState<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:
- 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 * UBase 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.
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 };