Appearance
Text
The Text component applies a typography role 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. To choose the element, use an element component such as Text.h1 or Text.p, or use the render prop (Base UI render pattern) to compose with another component.
Import
tsx
import { Text } from "@tailor-platform/app-shell";Basic Usage
tsx
function BasicUsage() {
return (
<div className="flex flex-col gap-1">
<Text.h2>Purchase orders</Text.h2>
<Text.p className="text-muted-foreground">Orders sent to suppliers this month.</Text.p>
</div>
);
}Props
Generated from the type surface — first-party props only.
TextProps
| Prop | Type | Default | Description |
|---|---|---|---|
ref? | React.Ref<HTMLElement> | - | Ref to the rendered element. |
render? | React.ReactElement | - | Custom element to render as (Base UI render prop). Use it to compose with another component. For a plain HTML element, use Text.h1 to Text.h6, Text.p, Text.span, or Text.div. |
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— constTextProps— type
Element Components
Text.h1 to Text.h6, Text.p, Text.span, and Text.div always render their named HTML element. Each one applies a default role, so you do not repeat variant for ordinary text:
tsx
function ElementComponents() {
return (
<div className="flex flex-col gap-1">
<Text.h3>Shipping address</Text.h3>
<Text.p className="text-muted-foreground">Orders ship from the main warehouse.</Text.p>
<Text.span className="tabular-nums">3 packages</Text.span>
</div>
);
}| Component | Default variant |
|---|---|
Text.h1 | heading-lg |
Text.h2 | heading-md |
Text.h3–Text.h6 | heading-sm |
Text.p | body-md |
Text.span | label-md |
Text.div | body-md |
The element sets the meaning and variant sets the appearance. The default is only a starting point, and any role works on any element:
tsx
<Text.h1 variant="heading-sm">A compact page title</Text.h1>
<Text.h3 variant="heading-lg">A visually prominent subsection</Text.h3>
<Text.span variant="body-sm">Updated recently</Text.span>The element components take variant, className, the element's own attributes, and a ref typed for the element (HTMLHeadingElement for Text.h2). They do not take render. For any other element, or to compose with another component, use Text with render.
Using render
All standard HTML attributes are also accepted by Text. Put an attribute that belongs to one element, such as htmlFor on a label, on the render element:
tsx
<Text variant="label-md" render={<label htmlFor="quantity" />}>
Quantity
</Text>Some lint rules report this example; see Lint.
Roles
variant takes one of the ten roles. On Text, 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 |
label-sm | span | Small label, badge text |
code-sm | span | ID and code. Text adds font-mono for you |
tsx
function Roles() {
return (
<div className="flex flex-col gap-3">
{/* The headings render a div so that this specimen is not a document heading. */}
<Text.div variant="heading-lg">heading-lg: Purchase orders</Text.div>
<Text.div variant="heading-md">heading-md: Line items</Text.div>
<Text.div variant="heading-sm">heading-sm: Shipping address</Text.div>
<Text variant="body-md">body-md: Orders sent to suppliers this month.</Text>
<Text variant="body-sm">body-sm: Updated 2 hours ago</Text>
<Text variant="label-md">label-md: Total amount</Text>
<Text variant="label-sm">label-sm: Draft</Text>
<Text variant="code-sm">code-sm: PO-2026-0001</Text>
</div>
);
}The sizes and line heights are in the Typography section of the styling guide.
Heading Level
On Text, 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 Text.h1 to Text.h6:
tsx
<Text.h3 variant="heading-md">Line items</Text.h3>To use a heading role for text that is not a heading, use Text.div or Text.span:
tsx
<Text.div variant="heading-md">1,240 orders</Text.div>Lint
Some jsx-a11y rules check that an element has text inside it, for example heading-has-content for <h3 /> and label-has-associated-control for <label />. These rules report the element in render as empty, because they do not see that Text passes its children to that element.
For headings, use Text.h1 to Text.h6. The rules do not report them. For other elements, such as label, use render, and if your app enables the rule, disable it on that line. ESLint and oxlint both read this comment:
tsx
// eslint-disable-next-line jsx-a11y/label-has-associated-control -- Text renders the children
<Text variant="label-md" render={<label htmlFor="quantity" />}>
Quantity
</Text>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-semibold for emphasis, and uppercase.
tsx
function ColorAndModifiers() {
return (
<div className="flex flex-col gap-3">
<Text variant="body-md" className="text-muted-foreground">
Orders sent this month.
</Text>
<Text variant="label-md" className="tabular-nums">
1,240.00
</Text>
</div>
);
}tsx
<Text variant="body-md" className="text-muted-foreground">
Orders sent this month.
</Text>
<Text.h3 variant="heading-md" className="leading-none">
Card title
</Text.h3>
<Text variant="label-md" className="tabular-nums">
1,240.00
</Text>Do not combine a role with other leading-*, tracking-*, or text-* size values. Pick a different variant instead. A text-* size class such as text-sm has no effect on Text: AppShell's stylesheet loads after yours, so the role's size wins.
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
<th className="text-label-md text-muted-foreground">Amount</th>TypeScript
typescript
import { type TextProps } from "@tailor-platform/app-shell";The element components have no exported props type. Read it from the component:
tsx
type SectionTitleProps = React.ComponentProps<typeof Text.h2>;Accessibility
- A heading role on
Textrenders a real heading (h2) by default, so screen readers list it with the other headings. Set the level withText.h1toText.h6so the levels follow the page structure. - Do not choose a heading level for its size.
Textkeeps the size and the level separate. body-*roles renderponText. Inside anotherp, useText.spaninstead (<Text.span variant="body-md">): the browser closes the outerpbefore the inner one, so React warns and server rendering fails to hydrate. Inside inline elements such asbutton,labelorspan, apis invalid HTML too, so render aspanthere as well.
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.
Related Components
- Card - The table above lists
heading-mdfor card titles - Layout - The table above lists
heading-lgfor the page title
AppShell's own components, including these two, render their titles and descriptions with Text.