Callout

Aside within documentation or long-form content. Use it to lift out a caveat, a

Content Callout.jsx Callout.d.ts Callout.md

Playground

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

Install and import

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

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

Aside within documentation or long-form content. Use it to lift out a caveat, a shortcut, or a prerequisite the reader would otherwise skim past.

Rules

  • Callout is for editorial content. For application state, such as a failed save or an expiring trial, use Alert or Banner. They announce to assistive technology; a callout does not.
  • One callout per section. Three in a row means the surrounding prose is not doing its job.
  • caution is for irreversible or costly actions. Do not use it for mild inconvenience.
  • Give it a title when the tone alone does not say why the reader should stop.
  • brand is not a feedback tone. Nothing is wrong and nothing needs your attention; it is the callout for a pull-quote, a feature highlight, or a moment the page is allowed to feel like the product rather than like a warning. Reach for tip first if the content is actually advice.
  • texture is a dot or line grid behind the fill, drawn in the border-strength colour so it never competes with the tone. It reads best on brand or note; a chromatic tone (tip, important, caution) is already carrying enough on its own.

Tradeoffs

Boxing content pulls the eye out of the reading flow. That is useful once per section and counterproductive when the page becomes a stack of boxes.

Props

The typed contract, from Callout.d.ts.

import * as React from "react";

/** Aside inside editorial content. */
export interface CalloutProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
  /**
   * note, tip, important and caution are feedback tones: pale, and matched to
   * a semantic role a reader already knows from Alert and Banner. brand is
   * not feedback. It reads --dt-surface-brand-muted, the same full-bleed
   * tint a hero band uses, for the one callout that is meant to feel like
   * the product rather than like a warning.
   * @default "note"
   */
  tone?: "note" | "tip" | "important" | "caution" | "brand";
  title?: React.ReactNode;
  children?: React.ReactNode;
  /** Lucide icon, or a sketch mark on a brand or editorial callout. */
  icon?: React.ReactNode;
  /**
   * Layers --dt-surface-texture, a dot or line grid, behind the tone's fill.
   * The pattern is drawn in the border-strength colour, never the brand hue,
   * so it never fights the tone underneath it.
   * @default false
   */
  texture?: boolean;
}

export declare function Callout(props: CalloutProps): React.JSX.Element;

Tokens it reads

Every custom property Callout.jsx resolves, read from the source: 27 semantic, 1 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-border-brandsemanticvar(--dt-color-primary-200)
--dt-border-defaultsemanticvar(--dt-color-neutral-200)
--dt-border-infosemanticvar(--dt-color-cyan-200)
--dt-border-successsemanticvar(--dt-color-green-200)
--dt-border-warningsemanticvar(--dt-color-amber-200)
--dt-border-width-defaultsemanticvar(--dt-dim-hair)
--dt-radius-containersemanticvar(--dt-radius-raw-16)
--dt-space-inline-smsemanticvar(--dt-dim-3)
--dt-space-inset-mdsemanticvar(--dt-dim-4)
--dt-space-stack-2xssemanticvar(--dt-dim-1)
--dt-surface-brand-mutedsemanticvar(--dt-color-primary-050)
--dt-surface-info-subtlesemanticvar(--dt-color-cyan-050)
--dt-surface-subtlesemanticvar(--dt-color-neutral-050)
--dt-surface-success-subtlesemanticvar(--dt-color-green-050)
--dt-surface-texturesemanticnone
--dt-surface-warning-subtlesemanticvar(--dt-color-amber-050)
--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-infosemanticvar(--dt-color-cyan-900)
--dt-text-label-md-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-md-sizesemanticvar(--dt-font-size-sm)
--dt-text-on-brand-mutedsemanticvar(--dt-color-primary-900)
--dt-text-primarysemanticvar(--dt-color-neutral-900)
--dt-text-secondarysemanticvar(--dt-color-neutral-600)
--dt-text-successsemanticvar(--dt-color-green-800)
--dt-text-warningsemanticvar(--dt-color-amber-900)
--dt-font-weight-semiboldprimitive600

Source

React only, no dependencies, every value a token.

import React from "react";

const TONES = {
  note: { bd: "var(--dt-border-default)", bg: "var(--dt-surface-subtle)", fg: "var(--dt-text-secondary)" },
  tip: { bd: "var(--dt-border-success)", bg: "var(--dt-surface-success-subtle)", fg: "var(--dt-text-success)" },
  important: { bd: "var(--dt-border-info)", bg: "var(--dt-surface-info-subtle)", fg: "var(--dt-text-info)" },
  caution: { bd: "var(--dt-border-warning)", bg: "var(--dt-surface-warning-subtle)", fg: "var(--dt-text-warning)" },
  brand: { bd: "var(--dt-border-brand)", bg: "var(--dt-surface-brand-muted)", fg: "var(--dt-text-on-brand-muted)" },
};

export function Callout({ tone = "note", title, children, icon, texture = false, style, ...rest }) {
  const t = TONES[tone] || TONES.note;
  /* --dt-surface-texture is a full background shorthand (image, position and
     size together), so it has to stay in the shorthand: background-image
     alone rejects a value carrying a size and silently renders nothing. A
     second, comma-separated layer is how one declaration paints the texture
     over the tone's own flat colour without a second element. */
  const fill = texture ? `var(--dt-surface-texture), ${t.bg}` : t.bg;
  return (
    <aside style={{
      display: "flex", gap: "var(--dt-space-inline-sm)",
      padding: "var(--dt-space-inset-md)",
      background: fill,
      borderRadius: "var(--dt-radius-container)",
      border: `var(--dt-border-width-default) solid ${t.bd}`,
      ...style,
    }} {...rest}>
      {icon && <span aria-hidden="true" style={{ color: t.fg, flex: "none", display: "flex", marginTop: 2 }}>{icon}</span>}
      <div style={{ minWidth: 0, display: "flex", flexDirection: "column", gap: "var(--dt-space-stack-2xs)" }}>
        {title && <span style={{ fontFamily: "var(--dt-text-label-md-family)", fontSize: "var(--dt-text-label-md-size)", fontWeight: "var(--dt-font-weight-semibold)", color: "var(--dt-text-primary)" }}>{title}</span>}
        <div style={{
          fontFamily: "var(--dt-text-body-sm-family)", fontSize: "var(--dt-text-body-sm-size)",
          lineHeight: "var(--dt-text-body-sm-line)", color: "var(--dt-text-secondary)", textWrap: "pretty",
        }}>{children}</div>
      </div>
    </aside>
  );
}

In context

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