Text

Running text in one of the system's type roles: an eyebrow, a lead paragraph, body copy, small print, fine print, a label. It replaces the hand-written .eyebrow, .lead and .fine classes every brand otherwise writes for itself.

Typography Text.jsx Text.d.ts Text.md

Playground

Change a prop and watch it. The code below follows along.

Text — Eyebrow, lead, body, small, fine and label, from the type roles.

Install and import

Text is in @dovetail-ds/react. Install it with React, if you haven't:

npm install @dovetail-ds/react react react-dom
import { Text } from "@dovetail-ds/react";

Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.

Guidelines

Running text in one of the system's type roles: an eyebrow, a lead paragraph, body copy, small print, fine print, a label. It replaces the hand-written .eyebrow, .lead and .fine classes every brand otherwise writes for itself.

Use it when

  • Any paragraph or line of text outside Prose that belongs to a type role.
  • A price, a count or a value that should line up: variant="label" numeric.

Don't use it when

  • It is long-form content from a CMS. Use Prose, which styles plain elements.
  • It is a heading. Use Heading.

Variants

variantroletagdefault tone
eyebroweyebrow, uppercasespansecondary
leadbody-lgpsecondary
bodybody-mdpinherit
smallbody-smpinherit
finebody-xsptertiary
labellabel-mdspaninherit

Example

<Stack gap="xs">
  <Text variant="label" weight="semibold" numeric>$48.00</Text>
  <Text variant="fine">Tax calculated at checkout.</Text>
</Stack>

Paragraph variants are bounded at --dt-measure-default so a line never runs wider than it can be read; pass measure="none" inside a narrow column where the bound does nothing.

Tone

brand and brand-secondary read --dt-text-brand and --dt-text-brand-secondary: an eyebrow, a highlighted word or a price in the brand colour, at text contrast in either mode.

<Text variant="eyebrow" tone="brand">New season</Text>

Every tone is a semantic role, so a Text inside a brand or photo Section follows the section: the section re-points --dt-text-primary, -secondary and -tertiary for its own surface.

Tokens

--dt-text-{role}-*, --dt-text-primary, -secondary, -tertiary, -link, --dt-measure-*, --dt-font-weight-*. The eyebrow and label roles read --dt-font-family-secondary, so a secondary face set in Configure reaches them.

Props

The typed contract, from Text.d.ts.

import * as React from "react";

/**
 * Running text in one of the system's type roles. The variant picks the role,
 * the tag and a default tone; every one of those can be overridden.
 */
export interface TextProps extends React.HTMLAttributes<HTMLElement> {
  /**
   * eyebrow: small caps label above a heading, secondary. lead: the opening
   * paragraph, body-lg, secondary. body: body-md. small: body-sm. fine: body-xs,
   * tertiary, for terms and footnotes. label: label-md, for a price or a value.
   * @default "body"
   */
  variant?: "eyebrow" | "lead" | "body" | "small" | "fine" | "label";
  /** Overrides the variant's default tone. inherit follows the surface it sits on; brand and brand-secondary read the brand text roles, for an eyebrow, a highlighted word or a price in the brand colour. */
  tone?: "inherit" | "primary" | "secondary" | "tertiary" | "link" | "brand" | "brand-secondary";
  /** Bounds the line length. Defaults to "default" for paragraph variants and "none" for eyebrow and label. */
  measure?: "narrow" | "default" | "wide" | "none";
  /** Overrides the role's weight, most often for a price. */
  weight?: "regular" | "medium" | "semibold";
  align?: React.CSSProperties["textAlign"];
  /** Tabular numerals, so a column of prices lines up. @default false */
  numeric?: boolean;
  /** Overrides the tag: p for paragraph variants, span for eyebrow and label. */
  as?: keyof React.JSX.IntrinsicElements;
  children?: React.ReactNode;
}

export declare function Text(props: TextProps): React.JSX.Element;

Tokens it reads

Every custom property Text.jsx resolves, read from the source: 150 semantic, 7 primitive. Component tier first, because that is the one to reach for.

  • semantic Shared roles. Override one and everything using that role moves with it.
  • primitive Raw values. A component reading one directly is a deliberate exception.
TokenTierDeclared as
--dt-text-artboard-body-linesemanticcalc(var(--dt-line-height-md) * 2.35)
--dt-text-artboard-body-sizesemanticcalc(var(--dt-font-size-md) * 2.5)
--dt-text-artboard-display-familysemanticvar(--dt-text-display-lg-family)
--dt-text-artboard-display-linesemanticcalc(var(--dt-line-height-7xl) * 2.55)
--dt-text-artboard-display-sizesemanticcalc(var(--dt-font-size-7xl) * 2.8)
--dt-text-artboard-display-trackingsemanticvar(--dt-tracking-tightest)
--dt-text-artboard-display-weightsemanticvar(--dt-text-display-lg-weight)
--dt-text-artboard-meta-sizesemanticcalc(var(--dt-font-size-sm) * 2.2)
--dt-text-artboard-meta-trackingsemanticvar(--dt-tracking-wider)
--dt-text-artboard-title-linesemanticcalc(var(--dt-line-height-7xl) * 1.7)
--dt-text-artboard-title-sizesemanticcalc(var(--dt-font-size-7xl) * 1.75)
--dt-text-body-lg-familysemanticvar(--dt-font-family-sans)
--dt-text-body-lg-linesemanticvar(--dt-line-height-lg)
--dt-text-body-lg-sizesemanticvar(--dt-font-size-lg)
--dt-text-body-lg-trackingsemanticvar(--dt-tracking-normal)
--dt-text-body-lg-weightsemanticvar(--dt-font-weight-regular)
--dt-text-body-md-familysemanticvar(--dt-font-family-sans)
--dt-text-body-md-linesemanticvar(--dt-line-height-md)
--dt-text-body-md-sizesemanticvar(--dt-font-size-md)
--dt-text-body-md-trackingsemanticvar(--dt-tracking-normal)
--dt-text-body-md-weightsemanticvar(--dt-font-weight-regular)
--dt-text-body-sm-familysemanticvar(--dt-font-family-sans)
--dt-text-body-sm-linesemanticvar(--dt-line-height-sm)
--dt-text-body-sm-sizesemanticvar(--dt-font-size-sm)
--dt-text-body-sm-trackingsemanticvar(--dt-tracking-normal)
--dt-text-body-sm-weightsemanticvar(--dt-font-weight-regular)
--dt-text-body-xs-familysemanticvar(--dt-font-family-sans)
--dt-text-body-xs-linesemanticvar(--dt-line-height-xs)
--dt-text-body-xs-sizesemanticvar(--dt-font-size-xs)
--dt-text-body-xs-trackingsemanticvar(--dt-tracking-normal)
--dt-text-body-xs-weightsemanticvar(--dt-font-weight-regular)
--dt-text-brandsemanticvar(--dt-color-primary-700)
--dt-text-brand-secondarysemanticvar(--dt-color-secondary-700)
--dt-text-code-md-familysemanticvar(--dt-font-family-mono)
--dt-text-code-md-linesemanticvar(--dt-line-height-sm)
--dt-text-code-md-sizesemanticvar(--dt-font-size-sm)
--dt-text-code-md-trackingsemanticvar(--dt-tracking-normal)
--dt-text-code-md-weightsemanticvar(--dt-font-weight-regular)
--dt-text-code-sm-familysemanticvar(--dt-font-family-mono)
--dt-text-code-sm-linesemanticvar(--dt-line-height-xs)
--dt-text-code-sm-sizesemanticvar(--dt-font-size-xs)
--dt-text-code-sm-trackingsemanticvar(--dt-tracking-normal)
--dt-text-code-sm-weightsemanticvar(--dt-font-weight-regular)
--dt-text-dangersemanticvar(--dt-color-red-800)
--dt-text-disabledsemanticvar(--dt-color-neutral-400)
--dt-text-display-2xl-familysemanticvar(--dt-font-family-sans)
--dt-text-display-2xl-linesemanticvar(--dt-line-height-fluid)
--dt-text-display-2xl-sizesemanticvar(--dt-font-size-fluid-2xl)
--dt-text-display-2xl-trackingsemanticvar(--dt-tracking-tightest)
--dt-text-display-2xl-weightsemanticvar(--dt-font-weight-medium)
--dt-text-display-lg-familysemanticvar(--dt-font-family-sans)
--dt-text-display-lg-linesemanticvar(--dt-line-height-7xl)
--dt-text-display-lg-sizesemanticvar(--dt-font-size-7xl)
--dt-text-display-lg-trackingsemanticvar(--dt-tracking-tightest)
--dt-text-display-lg-weightsemanticvar(--dt-font-weight-medium)
--dt-text-display-md-familysemanticvar(--dt-font-family-sans)
--dt-text-display-md-linesemanticvar(--dt-line-height-6xl)
--dt-text-display-md-sizesemanticvar(--dt-font-size-6xl)
--dt-text-display-md-trackingsemanticvar(--dt-tracking-tightest)
--dt-text-display-md-weightsemanticvar(--dt-font-weight-medium)
--dt-text-display-sm-familysemanticvar(--dt-font-family-sans)
--dt-text-display-sm-linesemanticvar(--dt-line-height-5xl)
--dt-text-display-sm-sizesemanticvar(--dt-font-size-5xl)
--dt-text-display-sm-trackingsemanticvar(--dt-tracking-tighter)
--dt-text-display-sm-weightsemanticvar(--dt-font-weight-medium)
--dt-text-display-xl-familysemanticvar(--dt-font-family-sans)
--dt-text-display-xl-linesemanticvar(--dt-line-height-fluid)
--dt-text-display-xl-sizesemanticvar(--dt-font-size-fluid-xl)
--dt-text-display-xl-trackingsemanticvar(--dt-tracking-tightest)
--dt-text-display-xl-weightsemanticvar(--dt-font-weight-medium)
--dt-text-eyebrow-familysemanticvar(--dt-font-family-secondary)
--dt-text-eyebrow-linesemanticvar(--dt-line-height-2xs)
--dt-text-eyebrow-sizesemanticvar(--dt-font-size-2xs)
--dt-text-eyebrow-trackingsemanticvar(--dt-tracking-wider)
--dt-text-eyebrow-weightsemanticvar(--dt-font-weight-semibold)
--dt-text-heading-lg-familysemanticvar(--dt-font-family-sans)
--dt-text-heading-lg-linesemanticvar(--dt-line-height-3xl)
--dt-text-heading-lg-sizesemanticvar(--dt-font-size-3xl)
--dt-text-heading-lg-trackingsemanticvar(--dt-tracking-tight)
--dt-text-heading-lg-weightsemanticvar(--dt-font-weight-medium)
--dt-text-heading-md-familysemanticvar(--dt-font-family-sans)
--dt-text-heading-md-linesemanticvar(--dt-line-height-2xl)
--dt-text-heading-md-sizesemanticvar(--dt-font-size-2xl)
--dt-text-heading-md-trackingsemanticvar(--dt-tracking-tight)
--dt-text-heading-md-weightsemanticvar(--dt-font-weight-medium)
--dt-text-heading-sm-familysemanticvar(--dt-font-family-sans)
--dt-text-heading-sm-linesemanticvar(--dt-line-height-xl)
--dt-text-heading-sm-sizesemanticvar(--dt-font-size-xl)
--dt-text-heading-sm-trackingsemanticvar(--dt-tracking-snug)
--dt-text-heading-sm-weightsemanticvar(--dt-font-weight-medium)
--dt-text-heading-xl-familysemanticvar(--dt-font-family-sans)
--dt-text-heading-xl-linesemanticvar(--dt-line-height-4xl)
--dt-text-heading-xl-sizesemanticvar(--dt-font-size-4xl)
--dt-text-heading-xl-trackingsemanticvar(--dt-tracking-tighter)
--dt-text-heading-xl-weightsemanticvar(--dt-font-weight-medium)
--dt-text-heading-xs-familysemanticvar(--dt-font-family-sans)
--dt-text-heading-xs-linesemanticvar(--dt-line-height-lg)
--dt-text-heading-xs-sizesemanticvar(--dt-font-size-lg)
--dt-text-heading-xs-trackingsemanticvar(--dt-tracking-normal)
--dt-text-heading-xs-weightsemanticvar(--dt-font-weight-medium)
--dt-text-headlinesemanticvar(--dt-text-primary)
--dt-text-infosemanticvar(--dt-color-cyan-900)
--dt-text-inversesemanticvar(--dt-color-neutral-050)
--dt-text-label-lg-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-lg-linesemanticvar(--dt-line-height-md)
--dt-text-label-lg-sizesemanticvar(--dt-font-size-md)
--dt-text-label-lg-trackingsemanticvar(--dt-tracking-normal)
--dt-text-label-lg-weightsemanticvar(--dt-font-weight-medium)
--dt-text-label-md-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-md-linesemanticvar(--dt-line-height-sm)
--dt-text-label-md-sizesemanticvar(--dt-font-size-sm)
--dt-text-label-md-trackingsemanticvar(--dt-tracking-normal)
--dt-text-label-md-weightsemanticvar(--dt-font-weight-medium)
--dt-text-label-sm-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-sm-linesemanticvar(--dt-line-height-xs)
--dt-text-label-sm-sizesemanticvar(--dt-font-size-xs)
--dt-text-label-sm-trackingsemanticvar(--dt-tracking-normal)
--dt-text-label-sm-weightsemanticvar(--dt-font-weight-medium)
--dt-text-on-actionsemanticvar(--dt-color-white)
--dt-text-on-action-brandsemanticvar(--dt-color-white)
--dt-text-on-action-brand-secondarysemanticvar(--dt-color-white)
--dt-text-on-action-dangersemanticvar(--dt-color-white)
--dt-text-on-action-disabledsemanticvar(--dt-color-neutral-400)
--dt-text-on-action-ghostsemanticvar(--dt-color-neutral-900)
--dt-text-on-action-secondarysemanticvar(--dt-color-neutral-900)
--dt-text-on-brandsemanticvar(--dt-color-white)
--dt-text-on-brand-mutedsemanticvar(--dt-color-primary-900)
--dt-text-on-brand-secondarysemanticvar(--dt-color-white)
--dt-text-on-brand-secondary-mutedsemanticvar(--dt-color-secondary-900)
--dt-text-on-dangersemanticvar(--dt-color-white)
--dt-text-on-infosemanticvar(--dt-color-white)
--dt-text-on-scrimsemanticvar(--dt-color-neutral-050)
--dt-text-on-scrim-brandsemanticvar(--dt-color-primary-200)
--dt-text-on-scrim-secondarysemanticvar(--dt-color-neutral-200)
--dt-text-on-scrim-strongsemanticvar(--dt-color-white)
--dt-text-on-selectedsemanticvar(--dt-color-neutral-900)
--dt-text-on-selected-brandsemanticvar(--dt-color-primary-900)
--dt-text-on-successsemanticvar(--dt-color-white)
--dt-text-on-warningsemanticvar(--dt-color-neutral-950)
--dt-text-primarysemanticvar(--dt-color-neutral-900)
--dt-text-secondarysemanticvar(--dt-color-neutral-600)
--dt-text-successsemanticvar(--dt-color-green-800)
--dt-text-tertiarysemanticvar(--dt-color-neutral-500)
--dt-text-warningsemanticvar(--dt-color-amber-900)
--dt-text-wordmarksemanticvar(--dt-text-primary)
--dt-color-secondary-700primitiveoklch(0.518 0.190 305)
--dt-font-weight-mediumprimitive500
--dt-font-weight-regularprimitive400
--dt-font-weight-semiboldprimitive600
--dt-measure-defaultprimitive68ch
--dt-measure-narrowprimitive45ch
--dt-measure-wideprimitive85ch

Source

React only, no dependencies, every value a token.

import React from "react";

const VARIANTS = {
  eyebrow: { role: "eyebrow", tag: "span", tone: "secondary", measure: "none", upper: true },
  lead: { role: "body-lg", tag: "p", tone: "secondary", measure: "default" },
  body: { role: "body-md", tag: "p", tone: "inherit", measure: "default" },
  small: { role: "body-sm", tag: "p", tone: "inherit", measure: "default" },
  fine: { role: "body-xs", tag: "p", tone: "tertiary", measure: "default" },
  label: { role: "label-md", tag: "span", tone: "inherit", measure: "none" },
};
const TONES = {
  inherit: "inherit",
  primary: "var(--dt-text-primary)",
  secondary: "var(--dt-text-secondary)",
  tertiary: "var(--dt-text-tertiary)",
  link: "var(--dt-text-link)",
  brand: "var(--dt-text-brand, var(--dt-text-link))",
  "brand-secondary": "var(--dt-text-brand-secondary, var(--dt-color-secondary-700, var(--dt-text-link)))",
};
const MEASURES = { narrow: "var(--dt-measure-narrow)", default: "var(--dt-measure-default)", wide: "var(--dt-measure-wide)", none: "none" };
const WEIGHTS = { regular: "var(--dt-font-weight-regular)", medium: "var(--dt-font-weight-medium)", semibold: "var(--dt-font-weight-semibold)" };

export function Text({ variant = "body", tone, measure, weight, align, numeric = false, as, children, style, ...rest }) {
  const v = VARIANTS[variant] || VARIANTS.body;
  const Tag = as || v.tag;
  const role = v.role;
  return (
    <Tag
      style={{
        margin: 0,
        fontFamily: `var(--dt-text-${role}-family)`,
        fontSize: `var(--dt-text-${role}-size)`,
        lineHeight: `var(--dt-text-${role}-line)`,
        fontWeight: WEIGHTS[weight] || `var(--dt-text-${role}-weight)`,
        letterSpacing: `var(--dt-text-${role}-tracking)`,
        textTransform: v.upper ? "uppercase" : undefined,
        color: TONES[tone || v.tone] || TONES.inherit,
        maxWidth: MEASURES[measure || v.measure] || MEASURES.none,
        textAlign: align,
        textWrap: v.tag === "p" ? "pretty" : undefined,
        fontVariantNumeric: numeric ? "tabular-nums" : undefined,
        ...style,
      }}
      {...rest}
    >
      {children}
    </Tag>
  );
}