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
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
defaultDismissed | boolean | false | Initial dismissed state in uncontrolled mode. |
dismissed | boolean | — | Controlled dismissed state. Omit to let the Callout manage it. |
dismissible | boolean | false | Shows a dismiss button when true. Uncontrolled by default (the Callout tracks its own dismissed state); pass `dismissed` + `onDismissedChange` to control it yourself. |
dismissLabel | string | Dismiss | Accessible label for the dismiss button. |
icon | ReactNode | — | Custom icon. Pass `false` to render no icon at all. Defaults to a built-in glyph for the current intent. |
intent | "info" | "success" | "warning" | "critical" | info | Which 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. |
restoreFocusRef | RefObject<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. |
title | ReactNode | — | Optional heading, rendered above `children`. |
Accessibility notes
roleis set for you, based on intent.warningandcriticalrender asrole="alert"(announced immediately, interrupting);infoandsuccessrender asrole="status"(announced politely). Pass your ownroleprop to override either — for example, a purely staticinfoCallout 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.titleandchildrenare 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; passrestoreFocusRefto 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.