Text
The primitive every other component points at through `render`. It is what stops a dialog title, a field label and a menu label from each growing their own typography class — seventeen title classes is exactly the duplication this project exists to remove.
npx shadcn@latest add @fragiola/textHeading level 1
Heading level 2
Heading level 3
This is a paragraph. The body uses the surface palette.
Bold text and small italic text in the same line.
A heading worn by another part
Secondary text uses text-palette-accent/85 — the measured point at which accent clears AA on every neutral surface in both themes.
Read the docs and .
Show code
import type { ReactNode } from "react";
import { Text } from "#/atoms/text";
// The floor is palette-surface. Text is the target every other component
// points at through `render` — the snippet below is the whole idea in one
// place: a heading that another component's part wears, keeping its a11y
// wiring while taking Text's typography. This is where secondary text
// (text-palette-accent/85) lives.
function WornHeading() {
return <Text.Heading as="h2">A heading worn by another part</Text.Heading>;
}
export default function TextDemo() {
return (
<div className="palette-surface flex flex-col gap-6 rounded-lg border border-palette-line bg-palette-base p-6">
<Row label="headings">
<div className="flex flex-col gap-3">
<Text.Heading as="h1">Heading level 1</Text.Heading>
<Text.Heading as="h2">Heading level 2</Text.Heading>
<Text.Heading as="h3">Heading level 3</Text.Heading>
</div>
</Row>
<Row label="paragraph + inline">
<div className="flex flex-col gap-3">
<Text.Paragraph>
This is a paragraph. The body uses the surface palette.
</Text.Paragraph>
<Text.Paragraph>
<Text.Strong>Bold text</Text.Strong> and{" "}
<Text.Small>small italic text</Text.Small> in the same
line.
</Text.Paragraph>
</div>
</Row>
<Row label="label + error + link">
<div className="flex flex-col gap-3">
<Text.Label>Field label</Text.Label>
<Text.Error>This is an error message.</Text.Error>
<Text.Link href="#">A plain link</Text.Link>
</div>
</Row>
<Row label="worn by another part">
<WornHeading />
</Row>
<Row label="secondary text">
<p className="text-sm text-palette-accent/85">
Secondary text uses <code>text-palette-accent/85</code> —
the measured point at which <code>accent</code> clears AA on
every neutral surface in both themes.
</p>
</Row>
<Row label="Text.Clickable">
<p className="text-sm text-palette-contrast">
Read the docs and{" "}
<Text.Clickable type="button">try it now</Text.Clickable>.
</p>
</Row>
</div>
);
}
function Row({ label, children }: { label: string; children: ReactNode }) {
return (
<div className="flex flex-col gap-2">
<span className="text-xs font-mono text-palette-accent/85">
{label}
</span>
{children}
</div>
);
}
What Text is for
A component part that needs a heading does not declare one — it wears Text.Heading through render, keeping the behaviour library's a11y wiring (the ids for aria-labelledby / aria-describedby) while taking Text's typography:
<DialogPrimitive.Title render={<Text.Heading as="h2" />} {...props} />Dialog.Title, Field.Label and Menu.Label all resolve to a Text member this way. If any of them grows its own typography class, the composition rule has been broken.
Secondary text
The most-used token in the shadcn baseline (muted-foreground, 44 occurrences) maps to a role Fragiola deliberately lacks. The settled value is text-palette-accent/85. The architecture initially proposed accent/70, but OKLCH→WCAG measurement showed 70% fails AA (4.5:1) even on neutral surfaces. At 85%, accent clears AA on every neutral surface (surface + raised, base + soft) in both themes — worst case 4.71:1. Secondary text realistically appears only on neutral backgrounds; chromatic palettes use contrast for their text, not a muted variant.
Text.Clickable vs Clickable.Button
Text.Clickable is a textual link rendered as a button — inline, underlined on hover, no fill. Clickable.Button is a filled affordance with a fill strategy, a measure and a form. They are different jobs; do not merge them.
Parts
| Part | Renders |
|---|---|
Heading | <h1|h2|h3>, text-palette-contrast |
Paragraph | <p>, text-palette-contrast |
Link | plain <a> — stitch a router link through render |
Label | <label>, block, semibold |
Error | <span> + palette-danger |
Strong / Small / Highlight | <strong> / <small> / <span> |
Clickable | <button>, inline textual link |
Density
Density is swappable per subtree. Tailwind v4's numeric spacing scale is
Clickable
The button surface — one component, three exports: `Button`, `Link` and `ExternalLink`. Every clickable affordance in the project composes against it: a dialog's close, an alert-dialog's action pair, a badge's dismiss, a field addon's button.