Skip to content
View as Markdown

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 ​

PropTypeDefaultDescription
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 — const
  • TextProps — 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>
  );
}
ComponentDefault variant
Text.h1heading-lg
Text.h2heading-md
Text.h3–Text.h6heading-sm
Text.pbody-md
Text.spanlabel-md
Text.divbody-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:

variantDefault elementUse
heading-lgh2Page title (Layout.Header)
heading-mdh2Section heading, card title
heading-smh2Sub-section heading
body-mdpBody copy, descriptions (default role)
body-smpCaption, timestamp, metadata
body-md-relaxedpLong text, such as a chat message or a document
body-sm-relaxedpLong text at the small size
label-mdspanField label, short UI text
label-smspanSmall label, badge text
code-smspanID 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 Text renders a real heading (h2) by default, so screen readers list it with the other headings. Set the level with Text.h1 to Text.h6 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 on Text. Inside another p, use Text.span instead (<Text.span variant="body-md">): the browser closes the outer p before the inner one, so React warns and server rendering fails to hydrate. Inside inline elements such as button, label or span, a p is invalid HTML too, so render a span there 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.

  • Card - The table above lists heading-md for card titles
  • Layout - The table above lists heading-lg for the page title

AppShell's own components, including these two, render their titles and descriptions with Text.