Prose

Wraps long-form editorial text (articles, documentation, changelogs) in a readable

Content Prose.jsx Prose.d.ts Prose.md

Playground

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

Install and import

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

npm install @dovetail-ds/react react react-dom
import { Prose } 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

Wraps long-form editorial text (articles, documentation, changelogs) in a readable measure with consistent vertical rhythm.

Rules

  • Cap the measure. The default 68ch is the upper bound for comfortable reading; do not raise it past 75ch to fill a wide column.
  • Prose owns the gap between blocks. Do not add margins to the paragraphs inside it.
  • Use lg for a standalone article page and md inside product UI. sm is for footnotes and sidebars only.
  • Content inside Prose comes from a CMS or Markdown. Do not put interactive controls in it; compose those outside.

Tradeoffs

A fixed measure leaves white space on wide screens. That space is the point: full-width text at 1400px is unreadable regardless of how empty the margins look.

Props

The typed contract, from Prose.d.ts.

import * as React from "react";

/** Readable long-form text container with a measure cap. */
export interface ProseProps extends React.HTMLAttributes<HTMLDivElement> {
  children?: React.ReactNode;
  /** Body role applied to the text. @default "md" */
  size?: "sm" | "md" | "lg";
  /** Maximum line length. @default "68ch" */
  measure?: string;
}

export declare function Prose(props: ProseProps): React.JSX.Element;

Tokens it reads

Every custom property Prose.jsx resolves, read from the source: 22 semantic. Component tier first, because that is the one to reach for.

  • semantic Shared roles. Override one and everything using that role moves with it.
TokenTierDeclared as
--dt-space-stack-mdsemanticvar(--dt-dim-4)
--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-primarysemanticvar(--dt-color-neutral-900)

Source

React only, no dependencies, every value a token.

import React from "react";

export function Prose({ children, size = "md", measure = "68ch", style, ...rest }) {
  const body = size === "lg" ? "lg" : size === "sm" ? "sm" : "md";
  return (
    <div
      className="dt-prose"
      style={{
        maxWidth: measure,
        fontFamily: `var(--dt-text-body-${body}-family)`,
        fontSize: `var(--dt-text-body-${body}-size)`,
        lineHeight: `var(--dt-text-body-${body}-line)`,
        color: "var(--dt-text-primary)",
        textWrap: "pretty",
        display: "flex", flexDirection: "column", gap: "var(--dt-space-stack-md)",
        ...style,
      }}
      {...rest}
    >
      {children}
    </div>
  );
}

In context

Prose also appears on the content reference cards, alongside the rest of its family.