> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Typography

> Create a clear typographic hierarchy that organizes content and guides people through the experience.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

export const DoDont = ({text, type, children}) => {
  const label = type === "do" ? "Do" : type === "dont" ? "Don\u2019t" : "Caution";
  const icon = type === "do" ? "check-circle" : type === "dont" ? "circle-xmark" : "triangle-exclamation";
  return <div className={`do-dont ${type}`}>
      {children && <div className="do-dont-content">{children}</div>}
      <div className={`do-dont-label do-dont-label-${type}`}>
        <Icon icon={icon} size={16} className="do-dont-label-icon" />
        <p>
          <strong>{label}</strong>
          {text && <span className="m-inline-start-1">{text}</span>}
        </p>
      </div>
    </div>;
};

## Usage guidelines

Follow Anvil2 guidance on casing, alignment, and color/contrast. Use AI Kit tokens consistently across all typography to maintain a cohesive type system.

***

## Type ramp

Use AI Kit tokens consistently across all typography to maintain a cohesive type system. Headline styles should follow the same sizing and font-weight conventions established in Anvil2 to ensure consistency across experiences.

<LiveCode example="ai-kit-typography-type-ramp" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Text } from "@servicetitan/anvil2-ai-kit";

  const cell = {
    padding: "12px 16px",
    textAlign: "left" as const,
    verticalAlign: "middle" as const,
  };
  const headerCell = {
    ...cell,
    fontSize: 12,
    fontWeight: 600,
    textTransform: "uppercase" as const,
    letterSpacing: "0.04em",
  };
  const rowBorder = { borderBottom: "1px solid #e5e7eb" };

  function App() {
    return (
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr style={rowBorder}>
            <th style={headerCell}>Name</th>
            <th style={headerCell}>Weight</th>
            <th style={headerCell}>Size/Line-height</th>
            <th style={headerCell}>Spacing/Rem</th>
          </tr>
        </thead>
        <tbody>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text variant="headline" el="h3" size="hero" text="Headline/Hero" />
            </td>
            <td style={cell}>bold (700)</td>
            <td style={cell}>32px/40px</td>
            <td style={cell}>-0.24% / -0.0048rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text
                variant="headline"
                el="h3"
                size="major"
                text="Headline/Major"
              />
            </td>
            <td style={cell}>bold (700)</td>
            <td style={cell}>24px/30px</td>
            <td style={cell}>-0.12% / -0.0018rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text
                variant="headline"
                el="h3"
                size="substantial"
                text="Headline/Substantial"
              />
            </td>
            <td style={cell}>bold (700)</td>
            <td style={cell}>20px/25px</td>
            <td style={cell}>-0.05% / -0.0006rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text
                variant="headline"
                el="h3"
                size="regular"
                text="Headline/Regular"
              />
            </td>
            <td style={cell}>bold (700)</td>
            <td style={cell}>16px/20px</td>
            <td style={cell}>0% / 0rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text size="featured" inline text="Body/featured" />
            </td>
            <td style={cell}>regular (400)</td>
            <td style={cell}>20px/27px</td>
            <td style={cell}>0% / 0rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text
                size="featured"
                inline
                text={<strong>Body/featured/emphasis</strong>}
              />
            </td>
            <td style={cell}>semi bold (600)</td>
            <td style={cell}>20px/27px</td>
            <td style={cell}>0% / 0rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text size="regular" inline text="Body/regular" />
            </td>
            <td style={cell}>regular (400)</td>
            <td style={cell}>16px/24px</td>
            <td style={cell}>0% / 0rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text
                size="regular"
                inline
                text={<strong>Body/regular/emphasis</strong>}
              />
            </td>
            <td style={cell}>semi bold (600)</td>
            <td style={cell}>16px/24px</td>
            <td style={cell}>0% / 0rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text size="fine" inline text="Body/fine" />
            </td>
            <td style={cell}>regular (400)</td>
            <td style={cell}>14px/21px</td>
            <td style={cell}>0% / 0rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text
                size="fine"
                inline
                text={<strong>Body/fine/emphasis</strong>}
              />
            </td>
            <td style={cell}>semi bold (600)</td>
            <td style={cell}>14px/21px</td>
            <td style={cell}>0% / 0rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text size="fine" inline text="Caption" />
            </td>
            <td style={cell}>regular (400)</td>
            <td style={cell}>12px/18px</td>
            <td style={cell}>0% / 0rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text size="fine" inline text={<strong>Caption/emphasis</strong>} />
            </td>
            <td style={cell}>semi bold (600)</td>
            <td style={cell}>12px/18px</td>
            <td style={cell}>0% / 0rem</td>
          </tr>
          <tr style={rowBorder}>
            <td style={cell}>
              <Text variant="eyebrow" text="Eyebrow" />
            </td>
            <td style={cell}>medium (500)</td>
            <td style={cell}>10px/15px</td>
            <td style={cell}>0% / 0rem</td>
          </tr>
        </tbody>
      </table>
    );
  }

  export default App;
  ```
</LiveCode>

## Type pairing guidance

Pair each headline level with its matching body size to keep hierarchy consistent across the product: a bigger headline earns a bigger supporting line, a smaller one stays compact.

<LiveCode example="ai-kit-typography-type-pairing" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Text } from "@servicetitan/anvil2-ai-kit";
  import { primitive } from "@servicetitan/anvil2-ai-kit/tokens";

  const column = {
    display: "flex" as const,
    flexDirection: "column" as const,
    gap: primitive.size["800"],
  };

  const stack = {
    display: "flex" as const,
    flexDirection: "column" as const,
    gap: primitive.size["200"],
  };

  const divider = {
    height: primitive.size["25"],
    backgroundColor: primitive.color.neutral["300"],
    border: "none" as const,
    margin: 0,
  };

  function App() {
    return (
      <div style={column}>
        <div style={stack}>
          <Text
            variant="headline"
            el="h3"
            size="major"
            text="Heading/body text"
          />
          <Text size="fine" text="Extra-Large 32px → Body Large 18px" />
          <Text
            variant="headline"
            el="h2"
            size="hero"
            text="Section title here"
          />
          <Text
            size="featured"
            text="Supporting body text that explains the section in a sentence or two."
          />
        </div>
        <div style={stack}>
          <Text size="fine" text="Large 24px → Body Base 16px" />
          <Text
            variant="headline"
            el="h3"
            size="major"
            text="Section title here"
          />
          <Text
            size="regular"
            text="Supporting body text that explains the section in a sentence or two."
          />
        </div>
        <hr style={divider} />
        <div style={stack}>
          <Text size="fine" text="Medium 20px → Body Small 14px" />
          <Text
            variant="headline"
            el="h3"
            size="substantial"
            text="Section title here"
          />
          <Text
            size="compact"
            text="Supporting body text that explains the section in a sentence or two."
          />
        </div>
        <div style={stack}>
          <Text size="fine" text="Small 16px → Caption 12px" />
          <Text
            variant="headline"
            el="h4"
            size="regular"
            text="Section title here"
          />
        </div>
      </div>
    );
  }

  export default App;
  ```
</LiveCode>

<LiveCode example="ai-kit-typography-type-pairing-title-subtitle-body" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Text } from "@servicetitan/anvil2-ai-kit";
  import { primitive } from "@servicetitan/anvil2-ai-kit/tokens";

  const column = {
    display: "flex" as const,
    flexDirection: "column" as const,
    gap: primitive.size["800"],
  };

  const stack = {
    display: "flex" as const,
    flexDirection: "column" as const,
    gap: primitive.size["200"],
  };

  function App() {
    return (
      <div style={column}>
        <Text
          variant="headline"
          el="h3"
          size="major"
          text="Title/subtitle/body text"
        />
        <div style={stack}>
          <Text size="fine" text="32 / 18 / 14px" />
          <Text
            variant="headline"
            el="h2"
            size="hero"
            text="Section title here"
          />
          <Text
            size="regular"
            text="Supporting body text that explains the section in a sentence or two."
          />
          <Text
            size="compact"
            text="Supporting body text that explains the section in a sentence or two."
          />
        </div>
        <div style={stack}>
          <Text size="fine" text="24 / 16 / 12px" />
          <Text
            variant="headline"
            el="h3"
            size="major"
            text="Section title here"
          />
          <Text
            size="regular"
            text="Supporting body text that explains the section in a sentence or two."
          />
          <Text
            size="fine"
            text="Supporting body text that explains the section in a sentence or two."
          />
        </div>
      </div>
    );
  }

  export default App;
  ```
</LiveCode>

<LiveCode example="ai-kit-typography-type-pairing-eyebrow-value-caption" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Text } from "@servicetitan/anvil2-ai-kit";
  import { primitive, semantic } from "@servicetitan/anvil2-ai-kit/tokens";

  const row = {
    display: "flex" as const,
    flexDirection: "row" as const,
    gap: primitive.size["800"],
    alignItems: "stretch" as const,
  };

  const column = {
    display: "flex" as const,
    flexDirection: "column" as const,
    gap: primitive.size["800"],
    flex: 1,
    minWidth: 0,
  };

  const stack = {
    display: "flex" as const,
    flexDirection: "column" as const,
    gap: primitive.size["200"],
  };

  const recipeLabel = {
    color: semantic.foreground.color.subtle,
  };

  const divider = {
    width: primitive.size["25"],
    alignSelf: "stretch" as const,
    backgroundColor: primitive.color.neutral["300"],
    flexShrink: 0,
  };

  function App() {
    return (
      <div style={row}>
        <div style={column}>
          <Text variant="headline" el="h3" size="major" text="Eyebrow/value" />
          <div style={row}>
            <div style={stack}>
              <div style={recipeLabel}>
                <Text size="fine" text="Smaller, denser UI" />
              </div>
              <Text variant="eyebrow" text="Total jobs" />
              <Text variant="headline" el="h3" size="major" text="4,821" />
            </div>
            <div style={stack}>
              <div style={recipeLabel}>
                <Text size="fine" text="Big dashboard numbers" />
              </div>
              <Text variant="eyebrow" text="Total jobs" />
              <Text variant="headline" el="h2" size="hero" text="4,821" />
            </div>
          </div>
        </div>
        <div style={divider} />
        <div style={column}>
          <Text
            variant="headline"
            el="h3"
            size="major"
            text="Eyebrow/value/caption"
          />
          <div style={row}>
            <div style={stack}>
              <div style={recipeLabel}>
                <Text size="fine" text="Smaller, denser UI" />
              </div>
              <Text variant="eyebrow" text="Total jobs" />
              <Text variant="headline" el="h3" size="major" text="4,821" />
              <Text size="fine" text="12% vs last period" />
            </div>
            <div style={stack}>
              <div style={recipeLabel}>
                <Text size="fine" text="Big dashboard numbers" />
              </div>
              <Text variant="eyebrow" text="Total jobs" />
              <Text variant="headline" el="h2" size="hero" text="4,821" />
              <Text size="fine" text="12% vs last period" />
            </div>
          </div>
        </div>
        <div style={divider} />
        <div style={column}>
          <Text variant="headline" el="h3" size="major" text="Empty state" />
          <div style={stack}>
            <Text
              variant="headline"
              el="h4"
              size="regular"
              text="No data available"
            />
            <Text
              size="fine"
              text="Try adjusting your filters or date range to see results"
            />
          </div>
        </div>
      </div>
    );
  }

  export default App;
  ```
</LiveCode>

## Do's and don'ts

<Columns cols={2} className="gap-4 items-stretch mb-4">
  <DoDont type="do" text="Use clearly differentiated heading sizes to establish a strong visual hierarchy and make content easy to scan.">
    <LiveCode example="ai-kit-typography-do-casing" screenshotOnly fullWidth>
      ```tsx lines theme={null}
      import { Text } from "@servicetitan/anvil2-ai-kit";
      import { primitive } from "@servicetitan/anvil2-ai-kit/tokens";

      const stack = {
        display: "flex" as const,
        flexDirection: "column" as const,
        gap: primitive.size["200"],
      };

      function App() {
        return (
          <div style={stack}>
            <Text variant="headline" el="h2" size="hero" text="Create impact" />
            <Text
              size="featured"
              text={<strong>Transforming possibilities into impact.</strong>}
            />
            <Text
              size="compact"
              text="Create smarter, more impactful solutions that inspire progress and drive meaningful change."
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>
  </DoDont>

  <DoDont type="dont" text="Don't use similar font sizes across heading levels. Keep a noticeable size difference so the hierarchy stays clear.">
    <LiveCode example="ai-kit-typography-dont-casing" screenshotOnly fullWidth>
      ```tsx lines theme={null}
      import { Text } from "@servicetitan/anvil2-ai-kit";
      import { primitive } from "@servicetitan/anvil2-ai-kit/tokens";

      const stack = {
        display: "flex" as const,
        flexDirection: "column" as const,
        gap: primitive.size["200"],
      };

      function App() {
        return (
          <div style={stack}>
            <Text variant="headline" el="h2" size="hero" text="Create impact" />
            <Text
              variant="headline"
              el="h3"
              size="major"
              text="Transforming possibilities into impact."
            />
            <Text
              size="compact"
              text="Create smarter, more impactful solutions that inspire progress and drive meaningful change."
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>
  </DoDont>
</Columns>

<Columns cols={2} className="gap-4 items-stretch">
  <DoDont type="do" text="Use a headline for the title and body text for supporting copy so the hierarchy stays clear.">
    <LiveCode example="ai-kit-typography-do-explore-new-ideas" screenshotOnly fullWidth>
      ```tsx lines theme={null}
      import { Text } from "@servicetitan/anvil2-ai-kit";
      import { primitive } from "@servicetitan/anvil2-ai-kit/tokens";

      const stack = {
        display: "flex" as const,
        flexDirection: "column" as const,
        gap: primitive.size["200"],
      };

      function App() {
        return (
          <div style={stack}>
            <Text variant="headline" el="h2" size="hero" text="Explore new ideas" />
            <Text
              size="featured"
              text="Discover simple ways to improve your workflow and create better experiences."
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>
  </DoDont>

  <DoDont type="dont" text="Don't set supporting copy in the same headline size as the title. Keep headline and body as separate styles.">
    <LiveCode example="ai-kit-typography-dont-explore-new-ideas" screenshotOnly fullWidth>
      ```tsx lines theme={null}
      import { Text } from "@servicetitan/anvil2-ai-kit";

      function App() {
        return (
          <Text
            variant="headline"
            el="h2"
            size="hero"
            text="Explore new ideas Discover simple ways to improve your workflow and create better experiences."
          />
        );
      }

      export default App;
      ```
    </LiveCode>
  </DoDont>
</Columns>

### Casing

Use title case for headlines and titles, and sentence case for body copy. Rely on [A2 Typography rules](/docs/web/foundations/typography) for casing.

### Alignment

Left-align body copy and headlines by default. Center only for short, standalone copy (empty states, single-line confirmations, modal titles). Right-align only for RTL locales or short supporting notes next to a left-aligned label — never a full paragraph. Baseline-align label/value pairs within a row or card.

### Color and contrast

Standard text must meet a 4.5:1 contrast ratio against its background. Large text (18.5px+ bold, or 24px+ regular) must meet 3:1. Check secondary/tertiary grays against both white and dark surfaces before shipping — lighter grays used below 12px are the highest-risk combination.

### Weight matrix

Sofia Pro: Bold (700) only, across all headline sizes (Extra-Large, Large, Medium, Small; page title remains Sofia Bold). Inter: Regular (body copy), Medium (Eyebrow only), Semibold (emphasis) — no Bold anywhere in either family.
