Call To Action
betaA next step offered in a card: a scene that shows it, what it is, why, how far along you are, and the step itself.
Preview
Invite your team
Take payment with bookings
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/call-to-action
Use it for
- Where the space would sit empty otherwise: a workspace still setting up, a feature nobody has tried yet.
- title says what the step is. text says why, in a line or two. Put a Scene in scene to show it.
- main is the step itself, a primary button with its icon. later is the quiet way out: "Skip" or "Later".
- Pass progress, like { done: 3, of: 4 }, when the step is one of a set.
- Give main a result, like { label: "Invites sent", onUndo, onCommit }, to show what the press did on the button, with Undo for 8 seconds.
Avoid it for
- More than one on a screen.
- Warnings or news. Use an alert.
- Steps people can't skip. Put them in the page's flow.
Props
- title
Required. What the step is: "Invite your team". It names the card for screen readers.
string
- text
Why, in a line or two.
ReactNode
- scene
A Scene, inset 6px from the card's edge.
ReactNode
- progress
How far along a set of steps is: a small ring and "3 of 4".
{ done: number; of: number }
- main
Required. The step itself: a brand button with its icon. icon is a name from Icon's set or any icon. A returned promise shows it's busy.
{ label: string; icon?: IconName | ReactNode; onClick?: () => void | Promise<unknown>; result?: ActionResult; loading?: boolean; disabled?: boolean }
- later
The quiet way out, a ghost button before the step.
{ label: string; onClick?: () => void }
- className
Extra classes on the card. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| title | string | - | Required. What the step is: "Invite your team". It names the card for screen readers. |
| text | ReactNode | - | Why, in a line or two. |
| scene | ReactNode | - | A Scene, inset 6px from the card's edge. |
| progress | { done: number; of: number } | - | How far along a set of steps is: a small ring and "3 of 4". |
| main | { label: string; icon?: IconName | ReactNode; onClick?: () => void | Promise<unknown>; result?: ActionResult; loading?: boolean; disabled?: boolean } | - | Required. The step itself: a brand button with its icon. icon is a name from Icon's set or any icon. A returned promise shows it's busy. |
| later | { label: string; onClick?: () => void } | - | The quiet way out, a ghost button before the step. |
| className | string | - | Extra classes on the card. Later classes win. |
Accessibility
- The card is a region named by its title.
- Tab goes to the way out, then the step. Both are buttons.
- Progress reads as "3 of 4 done". The ring is hidden, since the words say it.
- A result changes the step's own name, like "Invites sent. Press to undo.", and is said in a polite live region.
- Undo only undoes once 1.2 seconds have passed, so a double press never undoes. ⌘Z takes it back too.
Examples
With progress, a way out, and Undo
import { UserPlus } from "lucide-react";
<CallToAction
scene={
<Scene>
<SceneHint icon={<UserPlus />} label="Invite people" />
</Scene>
}
title="Invite your team"
text="They see the plan and pick up work, without asking you."
progress={{ done: 3, of: 4 }}
later={{ label: "Skip", onClick: () => console.log("skip") }}
main={{
label: "Invite people",
icon: <UserPlus />,
result: { label: "Invites sent", onUndo: () => {}, onCommit: () => console.log("send invites") },
}}
/>