Plan
ProThe card an assistant hands you before it acts: what it intends to do, as a short list of to-dos you can read, edit and approve. Deliberately quiet, so status lives in a small mark at the head of each row.
Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Anatomy#
Four blocks, in reading order. An eyebrow says what kind of card this is (PlanIcon + PlanLabel, with PlanActions trailing). A heading says what the plan will do. A PlanGroup holds the work as a flat list under its own quiet label and count. A footer carries the meta and the decision: ghost on the left, primary on the right, per the house rule.
import {
Plan,
PlanHeader,
PlanIcon,
PlanLabel,
PlanStatus,
PlanActions,
PlanHeading,
PlanTitle,
PlanDescription,
PlanGroup,
PlanGroupHeader,
PlanGroupTitle,
PlanGroupCount,
PlanSteps,
PlanStep,
PlanStepContent,
PlanStepTitle,
PlanStepDescription,
PlanStepInput,
PlanStepAction,
PlanStepEdit,
PlanStepTrigger,
PlanStepDetail,
PlanTasks,
PlanTask,
PlanStepSkeleton,
PlanMore,
PlanAddStep,
PlanFooter,
PlanSummary,
} from "@/components/ui/plan"
<Plan>
<PlanHeader>
<PlanIcon />
<PlanLabel />
<PlanStatus />
<PlanActions />
</PlanHeader>
<PlanHeading>
<PlanTitle />
<PlanDescription />
</PlanHeading>
<PlanGroup>
<PlanGroupHeader>
<PlanGroupTitle />
<PlanGroupCount />
</PlanGroupHeader>
<PlanSteps>
<PlanStep>
<PlanStepContent>
<PlanStepTitle />
<PlanStepDescription />
</PlanStepContent>
<PlanStepAction />
<PlanStepEdit />
</PlanStep>
<PlanStep collapsible>
<PlanStepTrigger>
<PlanStepContent>
<PlanStepInput />
</PlanStepContent>
</PlanStepTrigger>
<PlanStepDetail>
<PlanTasks>
<PlanTask />
</PlanTasks>
</PlanStepDetail>
</PlanStep>
<PlanStepSkeleton />
</PlanSteps>
<PlanMore />
<PlanAddStep />
</PlanGroup>
<PlanFooter>
<PlanSummary />
…
</PlanFooter>
</Plan>The card is the only container. Nothing inside it is boxed again and there are no dividers: groups are made by a label and whitespace, the way the Accordion's minimal variant makes rows. A panel inside a card reads as a slab in the dark themes and as a redundant line in the light ones, and every row shares the card's own left edge, so a plan has exactly one text axis from the eyebrow down to the footer. Every block is droppable.
To-do states#
Each PlanStep carries a status, and that mark plus one step of ink is the whole of its styling: a dashed ring while it waits, an ink spinner while it runs, a green check once it's done, a red warning when it isn't. Only the live row goes to full ink; a finished one fades back. No row fill, no rail, no numerals. A column of solid circles reads as a diagram, and this is a paragraph you happen to be able to click.
Collapsible to-dos#
Give a to-do collapsible and its PlanStepTrigger unfolds a PlanStepDetail with the sub-tasks it breaks down into. It wraps Radix Collapsible, so keyboard and ARIA come for free, and the panel opens on the DS collapsible keyframes. A plan mixes both shapes freely: rows with nothing to show stay plain. Sub-tasks are marked with a dash rather than a ring and sit a step quieter, so an unfolded row can never be mistaken for more to-dos of the plan itself.
Read-only, then edit#
A plan is read-only by default. That is how it's looked at nine times out of ten: you read what the assistant proposes and approve it. Editing is a mode you deliberately enter, so PlanStepEdit and PlanAddStep render nothing at all (not hidden, not transparent, not in the DOM) until the root gets editing. Leave them in your markup unconditionally and let the one flag decide; no call site can leave half the card in the wrong mode.
Inside edit mode, every row is already a field: there is no pencil to press first, because a pencil whose only job is to focus the input sitting under it is a button that shouldn't exist. Render a PlanStepInput in place of each PlanStepTitle: it inherits the row's type and its resting tone and paints no frame, so the rows sit exactly where they sat, in the ink they were in, and simply start taking keys. Ink arrives on the one you reach for or land in. The field grows with the copy, Escape cancels, and Enter is yours to wire; the demo opens the next to-do with it, the way every list editor behaves.
That leaves PlanStepEdit holding one button: remove. It fades in with the pointer (six rows each carrying a trash can is a toolbar, not a plan) but never away from the keyboard or from a touch screen. It is also out of the row's flow, which matters more than it sounds: in flow, a 32px button is taller than a 20px line, so every single-line to-do would grow to the button's height while a wrapped one stayed taller and the rhythm would fall apart the moment you pressed Edit. Instead the row reserves one uniform 36px gutter for it, so the list is sized by its copy in both modes.
Long lists and drafting#
A plan is something you scan, so a long one opens at a readable length and offers the rest: PlanMore is the row that closes the panel: “3 more” collapsed, “Show less” open. Which rows you actually render is yours; this is the control and the label.
While the model is still writing, drop PlanStepSkeleton rows in. They keep the list's exact geometry (the same mark column, the same row height), so the real to-dos land in place instead of pushing the panel around when they arrive. Vary width across rows so a draft reads organic rather than striped, and each row that mounts while the plan is drafting plays the DS entrance, which is all the cascade a stream needs: rows arrive one at a time already.
Status chip#
PlanStatus derives its chip from the root's status, so a label and a state can never drift apart, and the root sets aria-busy while the plan is still moving. It is optional on purpose: in the quiet default the eyebrow says what the card is and the footer says what happens next, and a chip on top of both is one badge too many. Reach for it when a plan sits in a feed where it has to announce itself.
Inside a conversation#
Set variant="plain" for a plan the assistant hands back inside a Chat turn or an AI Panel body. It drops the ring, the lift and the gutter, so the plan reads as part of the answer instead of a card inside a card. The panel keeps its own edge, which is all the structure the turn needs.
Density#
Like Card and Dialog, the Plan reads the density context (or a density prop). Here it tunes the row rhythm and the block gaps only. The card's 12px gutter and the panel's 4px inset are the concentric ladder (they set the panel's and the rows' corners), so they are fixed in both tiers, because a gutter that moves with density breaks that nesting in one of them.
API reference#
Plan
The card and the context every part reads. Forwards native <div> props. Every part accepts className, merged last, and every block is droppable.
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "drafting" | "review" | "running" | "complete" | "failed" | "review" | The plan's own state: drives PlanStatus, the row entrance while drafting, and aria-busy while drafting or running. |
| editing | boolean | false | Edit mode. A plan is read-only by default; PlanStepEdit and PlanAddStep render nothing until this is on. |
| variant | "card" | "plain" | "card" | card is the standalone artifact; plain drops the chrome and gutter for a Chat turn or an AI Panel. |
| density | "comfortable" | "compact" | - | Row rhythm, block gaps and padding. Inherits the nearest DensityProvider (compact with none). |
| asChild | boolean | false | Renders the child as the card via Radix Slot. |
| Data attribute | Description |
|---|---|
| data-status | The plan's status. |
| data-editing | Present in edit mode. |
PlanHeader
The eyebrow row: icon, label, then a status chip or actions. Forwards native <div> props.
PlanIcon
The identity mark: one muted glyph. aria-hidden. Renders a <span>.
PlanLabel
The eyebrow's word, e.g. Plan. Renders a <span>.
PlanActions
Trailing header slot for ghost icon buttons (regenerate, a menu). Forwards native <div> props.
PlanHeading
The title and description block. Forwards native <div> props.
PlanTitle
What the plan will do, in one line. Renders an <h3> and forwards its props.
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | false | Renders the child (another heading level) as the title. |
PlanDescription
The muted lines under the title. Forwards native <p> props.
PlanStatus
An optional chip worded and toned from the root's status. Forwards Badge props.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | BadgeProps["variant"] | - | Overrides the tone the status picks. |
| children | React.ReactNode | - | Overrides the wording (Drafting, Running, and so on). |
PlanGroup
A block of work, set apart by its label and whitespace, not a box. Forwards native <div> props.
PlanGroupHeader
The group's label row. Forwards native <div> props.
PlanGroupTitle
The group's quiet label, e.g. To-dos. Renders a <span>.
PlanGroupCount
The group's count beside its title. Renders a <span>.
PlanSteps
The to-do list, an <ol>.
PlanStep
One to-do, an <li> with its own 14px mark drawn from status. Forwards all its props.
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "pending" | "active" | "done" | "failed" | "pending" | The row's mark and ink. |
| collapsible | boolean | false | Turns the row into a Radix Collapsible: PlanStepTrigger opens a PlanStepDetail. |
| open | boolean | - | A collapsible step's open state, controlled. |
| defaultOpen | boolean | - | A collapsible step's initial state. |
| onOpenChange | (open: boolean) => void | - | Called when a collapsible step opens or closes. |
| Data attribute | Description |
|---|---|
| data-status | The step's status. |
| data-state | open or closed on a collapsible step. |
PlanStepContent
Wraps a row's text and fills, so the trailing slots ride the right edge. Forwards native <div> props.
PlanStepTitle
The to-do itself; muted, the live row steps up to full ink. Forwards native <div> props.
PlanStepDescription
An optional quieter second line. Forwards native <div> props.
PlanStepTrigger
The disclosure row of a collapsible step, with a turning caret. Forwards Radix Collapsible.Trigger props.
PlanStepDetail
What a collapsible step hides: sub-tasks, a note, a tool's output. Forwards Radix Collapsible.Content props.
PlanTasks
The sub-task list inside a step's detail, a <ul>.
PlanTask
One sub-task, an <li>.
| Prop | Type | Default | Description |
|---|---|---|---|
| done | boolean | false | The dash cross-fades to a green check. |
| Data attribute | Description |
|---|---|
| data-done | Present when done. |
PlanStepInput
The inline editor for a row's title in edit mode; it keeps the row's type, so nothing shifts. Shift+Enter breaks a line. Forwards <textarea> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | The to-do's text. |
| onValueChange* | (value: string) => void | - | Called on every edit. |
| onCommit | (value: string) => void | - | Enter or blur accepts the edit. |
| onCancel | () => void | - | Escape abandons it. |
PlanStepAction
What the row is telling you (a duration, a Badge, a Retry), always visible. Forwards native <div> props.
PlanStepEdit
What you can do to the row, in practice one Remove button. Renders nothing unless the plan is editing, then fades in with the pointer. Forwards native <div> props.
PlanStepSkeleton
The placeholder row while the plan is drafting, with the real row's geometry. Renders an <li>.
| Prop | Type | Default | Description |
|---|---|---|---|
| width | "full" | "long" | "short" | "full" | Vary it so a draft reads organic, not striped. |
PlanMore
The overflow row that closes a long list. Which rows you render is yours. Forwards native <button> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| expanded | boolean | false | Flips the label from {count} more to Show less. |
| count | number | - | How many rows are hidden, shown while collapsed. |
| Data attribute | Description |
|---|---|
| data-expanded | Present when expanded. |
PlanAddStep
The row that closes an editable list. Renders nothing outside edit mode. Forwards native <button> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | "Add step" | The row's label. |
PlanFooter
The commit bar: the summary left, the ghost and primary pair right. Forwards native <div> props.
PlanSummary
The footer's meta, e.g. 6 to-dos · about 4 min, in tabular-nums. Forwards native <p> props.