Skip to content

Callout

A contextual inline prompt — a tip, a warning, confirmation that something succeeded. Four intents (info, success, warning, critical), an optional dismiss control, up to two actions, and a default icon per intent that’s always aria-hidden — the title and body text carry the meaning, never the glyph.

Playground

Heads up

Your trial ends in 3 days.

Install

npm install @runmarklabs/react
/* your app's CSS entry point */
@import "@runmarklabs/react/callout/style.css";
import { Callout } from "@runmarklabs/react/callout";
<Callout intent="warning" title="Heads up" dismissible>
Your API key hasn't been used in 90 days.
</Callout>;

Props

PropTypeDefaultDescription
actions[ReactNode] | [ReactNode, ReactNode]Up to two actions, rendered after the body — pass a `Button` (or your own element), Callout only lays it out. The tuple type is the "up to two" limit enforced at compile time rather than a runtime check.
defaultDismissedbooleanfalseInitial dismissed state in uncontrolled mode.
dismissedbooleanControlled dismissed state. Omit to let the Callout manage it.
dismissiblebooleanfalseShows a dismiss button when true. Uncontrolled by default (the Callout tracks its own dismissed state); pass `dismissed` + `onDismissedChange` to control it yourself.
dismissLabelstringDismissAccessible label for the dismiss button.
iconReactNodeCustom icon. Pass `false` to render no icon at all. Defaults to a built-in glyph for the current intent.
intent"info" | "success" | "warning" | "critical"infoWhich of the four intents this callout communicates. Drives color, the default icon, the default `role`, and the `data-runmarklabs-intent` attribute consumers can use to target it in tests.
onDismissedChange((dismissed: boolean) => void)Called after the dismissed state changes, controlled or not.
restoreFocusRefRefObject<HTMLElement | null>Where focus goes after a dismissal that happened while focus was inside this Callout (clicking or activating-by-keyboard the dismiss button while it had focus). Omit to fall back to the Callout's parent element — never the document body. Has no effect if the dismissal happens because a controlling consumer changes the `dismissed` prop from somewhere else entirely (nothing inside the Callout ever had focus for this component to hand off from); that consumer owns restoring focus in that case.
titleReactNodeOptional heading, rendered above `children`.

Accessibility notes

  • role is set for you, based on intent. warning and critical render as role="alert" (announced immediately, interrupting); info and success render as role="status" (announced politely). Pass your own role prop to override either — for example, a purely static info Callout that’s part of the page’s initial content and shouldn’t announce itself as a live region at all.
  • The icon is always aria-hidden. title and children are the accessible name/description; a custom icon you pass in doesn’t change that.
  • Controlled and uncontrolled dismiss both go through the dismiss button, a native <button> — keyboard-operable (Tab, Enter, Space) and visibly focused with no extra work.
  • Dismissing moves focus somewhere sensible, never to <body>, when the dismiss button had focus at the time. Defaults to the Callout’s parent element; pass restoreFocusRef to send it somewhere more specific instead.
  • Zero axe violations across all four intents, dismissible or not — asserted in packages/react/src/Callout/Callout.test.tsx, not just eyeballed here.