Callout
Aside within documentation or long-form content. Use it to lift out a caveat, a
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall 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.
cautionis for irreversible or costly actions. Do not use it for mild inconvenience.- Give it a
titlewhen the tone alone does not say why the reader should stop. brandis 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 fortipfirst if the content is actually advice.textureis a dot or line grid behind the fill, drawn in the border-strength colour so it never competes with the tone. It reads best onbrandornote; 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.
| Token | Tier | Declared as |
|---|---|---|
--dt-border-brand | semantic | var(--dt-color-primary-200) |
--dt-border-default | semantic | var(--dt-color-neutral-200) |
--dt-border-info | semantic | var(--dt-color-cyan-200) |
--dt-border-success | semantic | var(--dt-color-green-200) |
--dt-border-warning | semantic | var(--dt-color-amber-200) |
--dt-border-width-default | semantic | var(--dt-dim-hair) |
--dt-radius-container | semantic | var(--dt-radius-raw-16) |
--dt-space-inline-sm | semantic | var(--dt-dim-3) |
--dt-space-inset-md | semantic | var(--dt-dim-4) |
--dt-space-stack-2xs | semantic | var(--dt-dim-1) |
--dt-surface-brand-muted | semantic | var(--dt-color-primary-050) |
--dt-surface-info-subtle | semantic | var(--dt-color-cyan-050) |
--dt-surface-subtle | semantic | var(--dt-color-neutral-050) |
--dt-surface-success-subtle | semantic | var(--dt-color-green-050) |
--dt-surface-texture | semantic | none |
--dt-surface-warning-subtle | semantic | var(--dt-color-amber-050) |
--dt-text-body-sm-family | semantic | var(--dt-font-family-sans) |
--dt-text-body-sm-line | semantic | var(--dt-line-height-sm) |
--dt-text-body-sm-size | semantic | var(--dt-font-size-sm) |
--dt-text-info | semantic | var(--dt-color-cyan-900) |
--dt-text-label-md-family | semantic | var(--dt-font-family-secondary) |
--dt-text-label-md-size | semantic | var(--dt-font-size-sm) |
--dt-text-on-brand-muted | semantic | var(--dt-color-primary-900) |
--dt-text-primary | semantic | var(--dt-color-neutral-900) |
--dt-text-secondary | semantic | var(--dt-color-neutral-600) |
--dt-text-success | semantic | var(--dt-color-green-800) |
--dt-text-warning | semantic | var(--dt-color-amber-900) |
--dt-font-weight-semibold | primitive | 600 |
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.