Fragiola UI
Atoms

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/text
headings

Heading level 1

Heading level 2

Heading level 3

paragraph + inline

This is a paragraph. The body uses the surface palette.

Bold text and small italic text in the same line.

label + error + link
This is an error message.A plain link
worn by another part

A heading worn by another part

secondary text

Secondary text uses text-palette-accent/85 — the measured point at which accent clears AA on every neutral surface in both themes.

Text.Clickable

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

PartRenders
Heading<h1|h2|h3>, text-palette-contrast
Paragraph<p>, text-palette-contrast
Linkplain <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

On this page