Text
Body copy and inline text with a fixed set of sizes, weights, and semantic colors.
Basic usage
Install the package:
pnpm add @design-schema/react @design-schema/tokens Import the stylesheet once for the app, then the component:
import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Text } from '@design-schema/react'; Examples
Default
- Default
Use the email you signed up with.
16px / 1rem · font.size.md
400 · font.weight.regular
#2a2b2f · #ecedee · #2d2b27 · #edddc0 · color.foreground
This story frames the component in a decorator — a background, a width — that this page does not run, so it is shown as source rather than rendered without its frame.
Truncate Inline16px / 1rem · font.size.md
400 · font.weight.regular
#2a2b2f · #ecedee · #2d2b27 · #edddc0 · color.foreground
- Body Copy
Changes are saved automatically. You can undo any change for 30 days.
16px / 1rem · font.size.md
400 · font.weight.regular
#2a2b2f · #ecedee · #2d2b27 · #edddc0 · color.foreground
- Caption
Last updated 2 minutes ago.
11px / 0.6875rem · 10px / 0.625rem · font.size.xs
400 · font.weight.regular
#56585c · #bcbec1 · #605747 · #ccbc9f · color.foreground.muted
- Inline Error WordingError: enter an email address like [email protected]
16px / 1rem · font.size.md
400 · font.weight.regular
#b51d26 · #ff958d · color.foreground.danger
size
- size="xs"
Use the email you signed up with.
11px / 0.6875rem · 10px / 0.625rem · font.size.xs
- size="sm"
Use the email you signed up with.
13px / 0.8125rem · font.size.sm
- size="md"
Use the email you signed up with.
16px / 1rem · font.size.md
- size="lg"
Use the email you signed up with.
19px / 1.1875rem · 20px / 1.25rem · font.size.lg
- size="xl"
Use the email you signed up with.
23px / 1.4375rem · 25px / 1.5625rem · font.size.xl
weight
- weight="regular"
Use the email you signed up with.
400 · font.weight.regular
- weight="medium"
Use the email you signed up with.
500 · font.weight.medium
- weight="semibold"
Use the email you signed up with.
600 · font.weight.semibold
- weight="bold"
Use the email you signed up with.
700 · font.weight.bold
tone
- tone="default"
Use the email you signed up with.
#2a2b2f · #ecedee · #2d2b27 · #edddc0 · color.foreground
- tone="strong"
Use the email you signed up with.
#000000 · #ffffff · #040302 · #f9eace · color.foreground.strong
- tone="muted"
Use the email you signed up with.
#56585c · #bcbec1 · #605747 · #ccbc9f · color.foreground.muted
- tone="danger"
Error: enter an email address like [email protected]
#b51d26 · #ff958d · color.foreground.danger
This story frames the component in a decorator — a background, a width — that this page does not run, so it is shown as source rather than rendered without its frame.
tone="onAction"#ffffff · #f9eace · color.foreground.onAction
align
- align="start"
Use the email you signed up with.
- align="center"
Use the email you signed up with.
- align="end"
Use the email you signed up with.
element
- element="p"
Use the email you signed up with.
- element="span"Use the email you signed up with.
truncate
This story frames the component in a decorator — a background, a width — that this page does not run, so it is shown as source rather than rendered without its frame.
truncateThis story frames the component in a decorator — a background, a width — that this page does not run, so it is shown as source rather than rendered without its frame.
truncate
Props
7 rows| Prop | Type | Default | Description |
|---|---|---|---|
| children required | content | — | The text content. Inline formatting (emphasis, links) is allowed; block elements are not. |
| size | xs | sm | md | lg | xl | md | Maps to the font size scale. `md` is body copy; `xs` is the smallest readable size and is reserved for captions and metadata. |
| weight | regular | medium | semibold | bold | regular | Emphasis without changing size. Prefer weight over color for hierarchy. |
| tone | default | strong | muted | danger | onAction | default | Semantic color. `onAction` is only for text placed on an action background, and its story paints that background (color.action.primary.background) behind the Text, on a wrapper holding a Box with `inset: md` around it. No component paints an action surface, so that wrapper is sanctioned story scaffolding like the truncate width: a plain element (a `div` on web and Lit, a View on native) whose background is the token itself. Enum stories use the Default children, "Use the email you signed up with.", except `ToneDanger`, which uses the `inline-error-wording` children verbatim on every platform so colour is never the only signal, and `ToneOnAction`, which reads "Text on an action background". The web modifier class is kebab-case (`ds-text--tone-on-action`), from an explicit tone-to-class table. There is no `inverse` tone: the shared foreground vocabulary has no such name, so an inverse surface re-scopes the foreground instead (see `styles.color`). Accessibility: Every tone meets 4.5:1 on the page background in every theme and mode except onAction, which is checked against color.action.primary.background. |
| align | start | center | end | start | Horizontal alignment. `start`/`end` follow writing direction. Native has no logical text alignment, so they resolve through I18nManager.isRTL at render; a direction change mid-session does not re-align text that is already on screen. Web and Lit use CSS `text-align: start|end`, which follows `dir` live. |
| truncate | boolean | false | Clip to one line with an ellipsis. On web the full text is exposed via `title` when children is a plain string; otherwise the consumer passes `title`, and neither one is a development warning — the text is then reachable only to a screen reader. A consumer `title` always wins and is forwarded unchanged, with or without `truncate`; an explicit `title={undefined}` counts as not passed, so the string children still supply it. On Lit there is no `title` property (the name is not attribute-safe): a `title` attribute the consumer puts on the host is observed and copied unchanged to the `part="text"` element, where it wins the same way, and is left on the host as well (Text never edits the consumer's DOM). Web and Lit therefore differ for rich children on purpose: web gets no automatic `title` unless children is a plain string, while Lit's textContent title covers slotted markup too. With no consumer title there is no "plain string" state, so `title` comes from the host's flattened, whitespace-collapsed textContent, is omitted when that is empty, sits on the `part="text"` element (the one that clips), and follows live edits to the text (a MutationObserver over the host's subtree, since slotchange misses character changes). With `element: span` the clipped box is `display: inline-block; max-inline-size: 100%; vertical-align: bottom`, so the width comes from the parent and the clipped box stays on the line; on the default `p` the clipped box stays `display: block`. A `TruncateInline` story (truncate with `element: span`) shows that case on web and Lit. Native clips with `numberOfLines={1}` and `ellipsizeMode="tail"` and has no affordance that reveals the rest — a known gap. Truncate stories need a width to clip against; that decorator (or render wrapper, which adds no args) is story scaffolding, not a binding, and may use a literal (`max-inline-size: 24ch` on web and Lit, `width: 200` on native). Accessibility: Truncated text is still read in full by screen readers; ensure sighted users can also reach it. |
| element | p | span | p | The HTML element to render — `p` for a block, `span` for inline. Labels and legends are native elements rendered by Input and Fieldset, which own the association; Text never renders one. |
Accessibility
Implicit ARIA role: generic.
Anatomy
What generation guarantees
Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.
This component does not animate, so it has no reduced-motion behaviour to describe.