```
+### Typography roles
+
+Text that a component renders takes its size, line height, weight, and letter spacing from a typography role, not from a raw size + weight combination. The roles are listed in the Typography section of `docs/concepts/styling-theming.md`.
+
+- Use the role utility (`astw:text-heading-md`, `astw:text-label-md`, ...) on the component's own element, or render `Text` when the element itself is the text.
+- Do not write new raw combinations such as `astw:text-lg astw:font-semibold` or `astw:text-sm astw:font-medium`. Existing ones are being moved to roles.
+- Combine a role only with the four modifiers: `astw:leading-none` (single-line text), `astw:tabular-nums`, `astw:font-medium` or `astw:font-semibold`, and `astw:uppercase`. Do not add other `leading-*`, `tracking-*`, or `text-[Npx]` values. 12px is the minimum size.
+- A role carries no color. Set the color with a token such as `astw:text-muted-foreground`.
+- A heading's level comes from the document structure, not from its size. Keep a title that is a heading in a heading element, and pick the role for its look. Do not change the element to get a different size, and do not turn a heading into a `div` to get a different line height. Under the `cream` and `bloom` palettes, `h1` to `h6` ignore `leading-none`; see the Typography section.
+
+```tsx
+// Good
+
{title}
+{updatedAt}
+```
+
+```tsx
+// Bad: a raw size + weight combination and a line height outside the roles
+
{title}
+```
+
### Container queries
Prefer Tailwind container-query utilities with arbitrary values.
diff --git a/.changeset/text-role-variants.md b/.changeset/text-role-variants.md
new file mode 100644
index 000000000..a19223bed
--- /dev/null
+++ b/.changeset/text-role-variants.md
@@ -0,0 +1,12 @@
+---
+"@tailor-platform/app-shell": minor
+---
+
+Add the `Text` component. It applies a typography role with the `variant` prop and does not set a color. Heading roles render `h2`, body roles render `p`, and label and code roles render `span`. Use the `render` prop to choose a different element, such as the heading level.
+
+```tsx
+import { Text } from "@tailor-platform/app-shell";
+
+}>Line items
+Orders sent this month.
+```
diff --git a/decisions/typography-roles.md b/decisions/typography-roles.md
index 400702edd..23ef6a41e 100644
--- a/decisions/typography-roles.md
+++ b/decisions/typography-roles.md
@@ -1,7 +1,7 @@
# Decision: typography role tokens
-> Status: **Decided for the first change — add 10 typography role tokens as CSS variables with Tailwind `text-*` utilities. No component uses them yet.**
-> Scope: role names and values, how they reach Tailwind, and the rules for combining them. This does not cover applying roles to AppShell components, a `Text` component, or sizes below 12px.
+> Status: **Decided for the first two changes — add 10 typography role tokens as CSS variables with Tailwind `text-*` utilities, then a `Text` component on top of them. AppShell's own components do not use the roles yet.**
+> Scope: role names and values, how they reach Tailwind, the rules for combining them, and the `Text` component (see "Second change" at the end). This does not cover applying roles to AppShell components or sizes below 12px.
## Context
@@ -84,5 +84,17 @@ Limit: this does not let a role replace a size that core sets on its own element
- **No visual change.** No component uses the roles yet. The default, cream, and bloom palettes render as before.
- **cream and bloom `h1` to `h6` rule.** Both palettes set `line-height: 1.2` and `letter-spacing: -0.03em` on `html :where(h1, h2, h3, h4, h5, h6)` outside any `@layer`. That rule wins over a role utility on those elements, so a heading role on an `h1` to `h6` gets the palette values for line height and letter spacing. It also wins over any `leading-*` or `tracking-*` class on those elements, including the `leading-none` in the card title pairing: an `h3` with `text-heading-md leading-none` has a line height of 1.2 under cream and bloom, and 1.0 under the default palette. The same classes on a `div` or `span` render as documented. This PR keeps the rule and documents the effect. Open question for the maintainers: what does this rule protect, and should it stay once heading roles exist?
- **Keyboard hint.** The earlier pairing table listed "keyboard hint" with `font-mono text-xs`. The only keyboard hint in the code (the sidebar ⌘K hint) uses `text-xs font-normal`. The new table has no keyboard hint row.
-- **Follow-up changes** (not in this PR): apply the roles to core components; `overline`; migrate 10px and 11px text to 12px; a `Text` component; numeric DataTable columns with `tabular-nums` and right alignment as the default; a MetricCard value role and a display role above 24px.
+- **Follow-up changes** (not in this PR): apply the roles to core components; `overline`; migrate 10px and 11px text to 12px; a `Text` component (added in a second change, see below); numeric DataTable columns with `tabular-nums` and right alignment as the default; a MetricCard value role and a display role above 24px.
- **Docs example.** The live example in the docs uses `div` and `p` elements. The docs app runs the bloom palette, and its `h1` to `h6` rule would otherwise change the heading rows.
+
+## Second change: the `Text` component
+
+> Status: **Decided.** Applying the roles to core components is a third change.
+
+- **API.** `Text` takes the role as `variant`. The prop is not called `role`, because `role` is an ARIA attribute. The element is set with Base UI's `render` prop, as in `Button`, the sidebar primitives, and the breadcrumb. AppShell has no `as` or `asChild` prop.
+- **Default elements.** The three heading roles render `h2`. The body roles, including the relaxed variants, render `p`. `label-md`, `label-sm`, and `code-sm` render `span`. All headings share one default so that no heading level is implied by size. `h2` is the default because a page has one `h1` (`Layout.Header`), and `Dialog.Title` from Base UI also renders `h2`. A default is hard to change later, because every call site without `render` would get a different element.
+- **Lint.** The `jsx-a11y/heading-has-content` rule reports `render={}` as an empty heading, and the function form `render={(props) => }` too. Apps that enable the rule disable it on that line. A `level` prop would avoid this, but it would add a second way to choose the element next to `render`. The documentation shows the disable comment instead.
+- **No color or modifier props.** Color and the four modifiers stay in `className`, as for the utilities. A consumer `leading-none`, `font-medium`, or `tracking-*` overrides the role even though AppShell's prefixed sheet loads later, because the role reads the shared `--tw-leading`, `--tw-font-weight`, and `--tw-tracking` variables first. `tests/typography-compile.test.ts` checks this.
+- **No `textVariants` export.** A role can already be applied with the `text-` utility or with `Text`. A third way would add public surface with no use case that these two do not cover. Unlike `Button` and `Badge`, `Text` does not export its cva function. It can be exported later without a breaking change.
+- **`code-sm` includes `font-mono`.** The utility cannot set the font family, but the component can.
+- **cream and bloom.** The `h1` to `h6` rule is unchanged. The default `h2` takes the palette's line height and letter spacing under these palettes, the same as existing headings in AppShell components today.
diff --git a/docs-manifest.json b/docs-manifest.json
index 749f4e4b3..ea7bfc404 100644
--- a/docs-manifest.json
+++ b/docs-manifest.json
@@ -1304,9 +1304,9 @@
"symbols": [],
"hashes": {
"typeSurface": null,
- "outline": "e63d837190e71ff1",
+ "outline": "8ad61f58de459be2",
"snapshot": null,
- "outputMd": "112b43c697d30369",
+ "outputMd": "c40075c1f34fb315",
"examples": "4d19f4f688e0f0ac"
}
},
@@ -1344,6 +1344,23 @@
"examples": "b07f258088e81cac"
}
},
+ "text": {
+ "slug": "text",
+ "kind": "code-backed",
+ "outline": "docs-src/components/text.docs.outline.md",
+ "output": "docs/components/text.md",
+ "examples": "docs-src/components/text.docs.examples.tsx",
+ "sources": ["packages/core/src/components/text/**"],
+ "claims": [],
+ "symbols": ["Text", "TextProps"],
+ "hashes": {
+ "typeSurface": "ed54a16ca677dbe3",
+ "outline": "b354c6be5f81ce2a",
+ "snapshot": "f5d9429ab497863a",
+ "outputMd": "6436f68b6b404f0b",
+ "examples": "4a832708dc79474b"
+ }
+ },
"textarea": {
"slug": "textarea",
"kind": "code-backed",
@@ -1676,7 +1693,7 @@
"packages/core/skills/app-shell-patterns/references/concepts/modules-and-resources.md": "bf15b5460f22d165",
"packages/core/skills/app-shell-patterns/references/concepts/routing-navigation.md": "8034c7f885994b29",
"packages/core/skills/app-shell-patterns/references/concepts/sidebar-navigation.md": "e342ef02dc2b9b86",
- "packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "112b43c697d30369",
+ "packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "c40075c1f34fb315",
"packages/core/skills/app-shell-patterns/references/components/action-panel.md": "71794d4b561e4d4b",
"packages/core/skills/app-shell-patterns/references/components/activity-card.md": "4f386e49380fb0b0",
"packages/core/skills/app-shell-patterns/references/components/ai-chat.md": "824d1aa36bb7f194",
@@ -1716,6 +1733,7 @@
"packages/core/skills/app-shell-patterns/references/components/spinner.md": "e05d94ba9c3d17da",
"packages/core/skills/app-shell-patterns/references/components/table.md": "f85ec64ceb7f4acf",
"packages/core/skills/app-shell-patterns/references/components/tabs.md": "30bb32e57fc78bd9",
+ "packages/core/skills/app-shell-patterns/references/components/text.md": "6436f68b6b404f0b",
"packages/core/skills/app-shell-patterns/references/components/textarea.md": "c940bc01cd573334",
"packages/core/skills/app-shell-patterns/references/components/timeline.md": "699249c9452485ab",
"packages/core/skills/app-shell-patterns/references/components/toolbar.md": "91d70b140c7bb747",
@@ -1757,6 +1775,6 @@
"packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md": "916baa3cd2b9cbf1",
"packages/core/skills/app-shell-patterns/references/pages/document-detail.md": "717ef4e7ae835aff",
"packages/core/skills/app-shell-patterns/references/migrations.md": "013c3fef2303b096",
- "packages/core/skills/app-shell-patterns/SKILL.md": "526decbfd2db6555"
+ "packages/core/skills/app-shell-patterns/SKILL.md": "f9d8e8f67c1b895e"
}
}
diff --git a/docs-src/components/text.docs.examples.tsx b/docs-src/components/text.docs.examples.tsx
new file mode 100644
index 000000000..fbf44b839
--- /dev/null
+++ b/docs-src/components/text.docs.examples.tsx
@@ -0,0 +1,45 @@
+import { Text } from "@tailor-platform/app-shell";
+
+export function BasicUsage() {
+ return (
+
+ Purchase orders
+ Orders sent to suppliers this month.
+
+ );
+}
+
+export function Roles() {
+ return (
+
+ {/* The headings render a div so that this specimen is not a document heading. */}
+ }>
+ heading-lg: Purchase orders
+
+ }>
+ heading-md: Line items
+
+ }>
+ heading-sm: Shipping address
+
+ body-md: Orders sent to suppliers this month.
+ body-sm: Updated 2 hours ago
+ label-md: Total amount
+ label-sm: Draft
+ code-sm: PO-2026-0001
+
+ );
+}
diff --git a/docs-src/components/text.docs.outline.md b/docs-src/components/text.docs.outline.md
new file mode 100644
index 000000000..2d599b6e6
--- /dev/null
+++ b/docs-src/components/text.docs.outline.md
@@ -0,0 +1,135 @@
+---
+kind: code-backed
+group: text
+title: Text
+description: Text with a typography role, rendered as an element you choose
+sources:
+ - packages/core/src/components/text/**
+---
+
+# Text
+
+The `Text` component applies a [typography role](../concepts/styling-theming.md#typography) to its content. A role sets font size, line height, font weight, and letter spacing together. The role does not set the color, and it does not choose the HTML element. Use the `render` prop to choose the element (Base UI render pattern).
+
+## Import
+
+```tsx
+import { Text } from "@tailor-platform/app-shell";
+```
+
+## Basic Usage
+
+
+
+
+
+All standard HTML attributes are also accepted. Put an attribute that belongs to one element, such as `htmlFor` on a `label`, on the `render` element:
+
+```tsx
+}>
+ Quantity
+
+```
+
+Some lint rules report this example; see [Lint](#lint).
+
+## Roles
+
+`variant` takes one of the ten roles. Each role renders a default element when you do not pass `render`:
+
+| `variant` | Default element | Use |
+| ----------------- | --------------- | ----------------------------------------------- |
+| `heading-lg` | `h2` | Page title (`Layout.Header`) |
+| `heading-md` | `h2` | Section heading, card title |
+| `heading-sm` | `h2` | Sub-section heading |
+| `body-md` | `p` | Body copy, descriptions (default role) |
+| `body-sm` | `p` | Caption, timestamp, metadata |
+| `body-md-relaxed` | `p` | Long text, such as a chat message or a document |
+| `body-sm-relaxed` | `p` | Long text at the small size |
+| `label-md` | `span` | Field label, short UI text, figures in a table |
+| `label-sm` | `span` | Small label, badge text |
+| `code-sm` | `span` | ID and code. `Text` adds `font-mono` for you |
+
+
+
+The sizes and line heights are in the [Typography section of the styling guide](../concepts/styling-theming.md#typography).
+
+## Heading Level
+
+All three heading roles render `h2` by default. The heading level comes from the document structure, not from the visual size, so set it with `render`:
+
+```tsx
+}>
+ Line items
+
+```
+
+To use a heading role for text that is not a heading, render a `div` or a `span`:
+
+```tsx
+}>
+ 1,240 orders
+
+```
+
+## Lint
+
+Some `jsx-a11y` rules check that an element has text inside it, for example `heading-has-content` for `` and `label-has-associated-control` for ``. These rules report the element in `render` as empty, because they do not see that `Text` passes its children to that element. If your app enables them, disable the rule on that line. ESLint and oxlint both read this comment:
+
+```tsx
+// eslint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children
+}>
+ Line items
+
+```
+
+## Color and Modifiers
+
+Set the color with a color token in `className`. Four modifiers may also be combined with a role: `leading-none` for single-line text, `tabular-nums`, `font-medium` or `font-semibold` for emphasis, and `uppercase`.
+
+
+
+```tsx
+
+ Orders sent this month.
+
+} className="leading-none">
+ Card title
+
+
+ 1,240.00
+
+```
+
+Do not combine a role with other `leading-*`, `tracking-*`, or `text-*` size values. Pick a different `variant` instead.
+
+## Using Roles Without Text
+
+Each role is also a plain utility, for example `text-heading-md`. Use the utility when you style your own element and do not need `Text`:
+
+```tsx
+
Amount
+```
+
+## TypeScript
+
+```typescript
+import { type TextProps } from "@tailor-platform/app-shell";
+```
+
+## Accessibility
+
+- A heading role renders a real heading (`h2`) by default, so screen readers list it with the other headings. Set the level with `render` so the levels follow the page structure.
+- Do not choose a heading level for its size. `Text` keeps the size and the level separate.
+- `body-*` roles render `p`. Inside another `p`, render a `span` instead, because a `p` cannot contain a `p`.
+
+## Palettes
+
+The `cream` and `bloom` palettes set the line height and letter spacing of `h1` to `h6` elements. Under these palettes, a heading role on a heading element uses the palette values (line height 1.2, letter spacing `-0.03em`), including when you add `leading-none`. The same role on a `div` or `span` uses the role values. See the [Typography section of the styling guide](../concepts/styling-theming.md#typography).
+
+## Related Components
+
+- [Card](./card.md) - The table above lists `heading-md` for card titles
+- [Layout](./layout.md) - The table above lists `heading-lg` for the page title
+
+AppShell's own components, including these two, still use the stock utilities. They will move to the roles in a later change.
diff --git a/docs-src/concepts/styling-theming.docs.outline.md b/docs-src/concepts/styling-theming.docs.outline.md
index c2d5d721e..460ceedc8 100644
--- a/docs-src/concepts/styling-theming.docs.outline.md
+++ b/docs-src/concepts/styling-theming.docs.outline.md
@@ -271,7 +271,7 @@ Pair a role with a color token from the Color section above. These pairings are
| --------------------------------- | ---------------------------------------------- |
| Page title (`Layout.Header`) | `text-heading-lg` |
| Section heading | `text-heading-md` |
-| Card title (`div` or `span`) | `text-heading-md leading-none` |
+| Card title | `text-heading-md leading-none` |
| Body copy | `text-body-md` |
| Secondary copy, descriptions | `text-body-md text-muted-foreground` |
| Caption, timestamp, metadata | `text-body-sm text-muted-foreground` |
@@ -286,12 +286,19 @@ Pair a role with a color token from the Color section above. These pairings are
Updated 2h ago
```
+The [`Text`](../components/text.md) component applies the same roles with a `variant` prop. It renders a default element for each role (`h2` for headings, `p` for body text, `span` for the others), and its `render` prop sets the element, such as the heading level:
+
+```tsx
+}>Line items
+Description copy
+```
+
On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's line height and letter spacing, including `leading-none`; see "Change a role" below. AppShell's own components still use the stock utilities (`text-sm`, `font-medium`, and so on) today. They will move to the roles in later changes, so the two sets can appear side by side for now.
**Rules**
- **A role carries no color.** Set the color with a token such as `text-muted-foreground`.
-- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size.
+- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size. With `Text`, set the element with `render`.
- **A role sets weight and letter spacing on its own element.** It does not inherit them from a parent. Put an emphasis or tracking class on the element that has the role, not on its parent.
- **Four modifiers may be combined with a role.** All are stock Tailwind utilities:
- Compact line height: `leading-none`. Use it only for single-line text, such as a card title. Write it after the role when you merge with `cn()`.
diff --git a/docs/components/text.md b/docs/components/text.md
new file mode 100644
index 000000000..7caa53527
--- /dev/null
+++ b/docs/components/text.md
@@ -0,0 +1,198 @@
+---
+title: Text
+description: Text with a typography role, rendered as an element you choose
+---
+
+
+
+# Text
+
+The `Text` component applies a [typography role](../concepts/styling-theming.md#typography) to its content. A role sets font size, line height, font weight, and letter spacing together. The role does not set the color, and it does not choose the HTML element. Use the `render` prop to choose the element (Base UI render pattern).
+
+## Import
+
+```tsx
+import { Text } from "@tailor-platform/app-shell";
+```
+
+## Basic Usage
+
+
+
+```tsx
+function BasicUsage() {
+ return (
+
+ Purchase orders
+ Orders sent to suppliers this month.
+
+ );
+}
+```
+
+## Props
+
+_Generated from the type surface — first-party props only._
+
+### TextProps
+
+| Prop | Type | Default | Description |
+| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ |
+| `ref?` | `React.Ref` | - | Ref to the rendered element. |
+| `render?` | `React.ReactElement` | - | Custom element to render as (Base UI render prop). Use it to set the heading level, for example `render={}`. |
+| `variant?` | `"heading-lg" \| "heading-md" \| "heading-sm" \| "body-md" \| "body-sm" \| "label-md" \| "label-sm" \| "code-sm" \| "body-md-relaxed" \| "body-sm-relaxed" \| null` | `"body-md"` | Typography role. Sets font size, line height, font weight, and letter spacing together. |
+
+## Exports
+
+- `Text` — function
+- `TextProps` — type
+
+All standard HTML attributes are also accepted. Put an attribute that belongs to one element, such as `htmlFor` on a `label`, on the `render` element:
+
+```tsx
+}>
+ Quantity
+
+```
+
+Some lint rules report this example; see [Lint](#lint).
+
+## Roles
+
+`variant` takes one of the ten roles. Each role renders a default element when you do not pass `render`:
+
+| `variant` | Default element | Use |
+| ----------------- | --------------- | ----------------------------------------------- |
+| `heading-lg` | `h2` | Page title (`Layout.Header`) |
+| `heading-md` | `h2` | Section heading, card title |
+| `heading-sm` | `h2` | Sub-section heading |
+| `body-md` | `p` | Body copy, descriptions (default role) |
+| `body-sm` | `p` | Caption, timestamp, metadata |
+| `body-md-relaxed` | `p` | Long text, such as a chat message or a document |
+| `body-sm-relaxed` | `p` | Long text at the small size |
+| `label-md` | `span` | Field label, short UI text, figures in a table |
+| `label-sm` | `span` | Small label, badge text |
+| `code-sm` | `span` | ID and code. `Text` adds `font-mono` for you |
+
+
+
+```tsx
+function Roles() {
+ return (
+
+ {/* The headings render a div so that this specimen is not a document heading. */}
+ }>
+ heading-lg: Purchase orders
+
+ }>
+ heading-md: Line items
+
+ }>
+ heading-sm: Shipping address
+
+ body-md: Orders sent to suppliers this month.
+ body-sm: Updated 2 hours ago
+ label-md: Total amount
+ label-sm: Draft
+ code-sm: PO-2026-0001
+
+ );
+}
+```
+
+The sizes and line heights are in the [Typography section of the styling guide](../concepts/styling-theming.md#typography).
+
+## Heading Level
+
+All three heading roles render `h2` by default. The heading level comes from the document structure, not from the visual size, so set it with `render`:
+
+```tsx
+}>
+ Line items
+
+```
+
+To use a heading role for text that is not a heading, render a `div` or a `span`:
+
+```tsx
+}>
+ 1,240 orders
+
+```
+
+## Lint
+
+Some `jsx-a11y` rules check that an element has text inside it, for example `heading-has-content` for `` and `label-has-associated-control` for ``. These rules report the element in `render` as empty, because they do not see that `Text` passes its children to that element. If your app enables them, disable the rule on that line. ESLint and oxlint both read this comment:
+
+```tsx
+// eslint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children
+}>
+ Line items
+
+```
+
+## Color and Modifiers
+
+Set the color with a color token in `className`. Four modifiers may also be combined with a role: `leading-none` for single-line text, `tabular-nums`, `font-medium` or `font-semibold` for emphasis, and `uppercase`.
+
+
+
+```tsx
+function ColorAndModifiers() {
+ return (
+
+
+ Orders sent this month.
+
+
+ 1,240.00
+
+
+ );
+}
+```
+
+```tsx
+
+ Orders sent this month.
+
+} className="leading-none">
+ Card title
+
+
+ 1,240.00
+
+```
+
+Do not combine a role with other `leading-*`, `tracking-*`, or `text-*` size values. Pick a different `variant` instead.
+
+## Using Roles Without Text
+
+Each role is also a plain utility, for example `text-heading-md`. Use the utility when you style your own element and do not need `Text`:
+
+```tsx
+
Amount
+```
+
+## TypeScript
+
+```typescript
+import { type TextProps } from "@tailor-platform/app-shell";
+```
+
+## Accessibility
+
+- A heading role renders a real heading (`h2`) by default, so screen readers list it with the other headings. Set the level with `render` so the levels follow the page structure.
+- Do not choose a heading level for its size. `Text` keeps the size and the level separate.
+- `body-*` roles render `p`. Inside another `p`, render a `span` instead, because a `p` cannot contain a `p`.
+
+## Palettes
+
+The `cream` and `bloom` palettes set the line height and letter spacing of `h1` to `h6` elements. Under these palettes, a heading role on a heading element uses the palette values (line height 1.2, letter spacing `-0.03em`), including when you add `leading-none`. The same role on a `div` or `span` uses the role values. See the [Typography section of the styling guide](../concepts/styling-theming.md#typography).
+
+## Related Components
+
+- [Card](./card.md) - The table above lists `heading-md` for card titles
+- [Layout](./layout.md) - The table above lists `heading-lg` for the page title
+
+AppShell's own components, including these two, still use the stock utilities. They will move to the roles in a later change.
diff --git a/docs/concepts/styling-theming.md b/docs/concepts/styling-theming.md
index 61a21f8dc..45b687988 100644
--- a/docs/concepts/styling-theming.md
+++ b/docs/concepts/styling-theming.md
@@ -288,7 +288,7 @@ Pair a role with a color token from the Color section above. These pairings are
| --------------------------------- | ---------------------------------------------- |
| Page title (`Layout.Header`) | `text-heading-lg` |
| Section heading | `text-heading-md` |
-| Card title (`div` or `span`) | `text-heading-md leading-none` |
+| Card title | `text-heading-md leading-none` |
| Body copy | `text-body-md` |
| Secondary copy, descriptions | `text-body-md text-muted-foreground` |
| Caption, timestamp, metadata | `text-body-sm text-muted-foreground` |
@@ -401,12 +401,19 @@ function TypographyRoles() {
Updated 2h ago
```
+The [`Text`](../components/text.md) component applies the same roles with a `variant` prop. It renders a default element for each role (`h2` for headings, `p` for body text, `span` for the others), and its `render` prop sets the element, such as the heading level:
+
+```tsx
+}>Line items
+Description copy
+```
+
On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's line height and letter spacing, including `leading-none`; see "Change a role" below. AppShell's own components still use the stock utilities (`text-sm`, `font-medium`, and so on) today. They will move to the roles in later changes, so the two sets can appear side by side for now.
**Rules**
- **A role carries no color.** Set the color with a token such as `text-muted-foreground`.
-- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size.
+- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size. With `Text`, set the element with `render`.
- **A role sets weight and letter spacing on its own element.** It does not inherit them from a parent. Put an emphasis or tracking class on the element that has the role, not on its parent.
- **Four modifiers may be combined with a role.** All are stock Tailwind utilities:
- Compact line height: `leading-none`. Use it only for single-line text, such as a card title. Write it after the role when you merge with `cn()`.
diff --git a/packages/core/__snapshots__/src__components__text__text.test.tsx.snap b/packages/core/__snapshots__/src__components__text__text.test.tsx.snap
new file mode 100644
index 000000000..a9b4e38ea
--- /dev/null
+++ b/packages/core/__snapshots__/src__components__text__text.test.tsx.snap
@@ -0,0 +1,9 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Text > snapshots > code role 1`] = `"PO-0001"`;
+
+exports[`Text > snapshots > default role 1`] = `"