Plan

Pro

The 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.

Plan overview

Marketing site analytics

Replace the legacy tracking with a consented, typed event schema. Includes a safe rollout path for production.

To-dos6
  1. Audit the current tracking setup
    Not started
  2. Add a consent banner
    Not started
  3. Map the new event schema
    Not started

6 to-dos · about 6 min

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Activate your Pro license (once per machine)npx koalaui-cli@latest login <key># Add this component (its dependencies come along)npx koalaui-cli@latest add plan

Usage#

This example is part of Koala UI Pro

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.

Plan overview

Refresh the pricing page

Rewrite the three tiers and the feature matrix in plain language.

To-dos3
  1. Pull the three current tiers from the CMS
    Not started
  2. Rewrite the feature matrix
    Not started
  3. Swap the annual toggle copy
    Not started

3 to-dos · about 2 min

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.

Plan overview

Refresh the pricing page

Rewrite the three tiers and the feature matrix in plain language.

To-dos4
  1. Collect the merged pull requests
    Done
  2. Draft the highlights
    In progress
  3. Attach the screenshots
    Failed
  4. Publish to the changelog
    Not started

2 of 4

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.

Plan overview

Migrate the changelog to MDX

Open a to-do to see what it's doing.

To-dos4
  1. Done
    • Port the frontmatterDone
    • Rewrite inline HTML as componentsDone
    • Move the images into public/changelogNot started
    • Check every internal link still resolvesNot started
    In progress
  2. Not started
  3. Redirect the old URLs
    Not started

2 of 4

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.

Plan overview

Refresh the pricing page

Read it through, then approve.

To-dos3
  1. Pull the three current tiers from the CMS
    Not started
  2. Rewrite the feature matrix in plain language
    Not started
  3. Swap the annual toggle copy
    Not started

3 to-dos · about 2 min

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.

Plan overview

Refresh the pricing page

Rewrite the three tiers and the feature matrix in plain language.

To-dos6
  1. Pull the three current tiers from the CMS
    Not started
  2. Rewrite the feature matrix
    Not started
  3. Swap the annual toggle copy
    Not started

6 to-dos · about 6 min

Plan overviewDrafting

Clean up the component registry

To-dos

Writing the plan…

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.

Plan overviewReady for review

Refresh the pricing page

  1. Pull the three current tiers
    Not started
  2. Rewrite the feature matrix
    Not started
Plan overviewRunning

Refresh the pricing page

  1. Pull the three current tiers
    Done
  2. Rewrite the feature matrix
    In progress
Plan overviewComplete

Refresh the pricing page

  1. Pull the three current tiers
    Done
  2. Rewrite the feature matrix
    Done
Plan overviewFailed

Refresh the pricing page

  1. Pull the three current tiers
    Done
  2. Rewrite the feature matrix
    Failed

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.

Can you get the docs search working across every component page?

Here's how I'd approach it. Say the word and I'll start.

To-dos3
  1. Index every MDX page at build time
    Not started
  2. Wire the index into the command palette
    Not started
  3. Add keyboard-only tests
    Not started

3 to-dos · about 4 min

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.

Comfortable

Fix the failing suite

To-dos3
  1. Read the failing test
    Done
  2. Patch the reducer
    In progress
  3. Re-run the suite
    Not started
Compact

Fix the failing suite

To-dos3
  1. Read the failing test
    Done
  2. Patch the reducer
    In progress
  3. Re-run the suite
    Not started

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.

PropTypeDefaultDescription
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.
editingbooleanfalseEdit 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).
asChildbooleanfalseRenders the child as the card via Radix Slot.
Data attributeDescription
data-statusThe plan's status.
data-editingPresent 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.

PropTypeDefaultDescription
asChildbooleanfalseRenders 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.

PropTypeDefaultDescription
variantBadgeProps["variant"]-Overrides the tone the status picks.
childrenReact.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.

PropTypeDefaultDescription
status"pending" | "active" | "done" | "failed""pending"The row's mark and ink.
collapsiblebooleanfalseTurns the row into a Radix Collapsible: PlanStepTrigger opens a PlanStepDetail.
openboolean-A collapsible step's open state, controlled.
defaultOpenboolean-A collapsible step's initial state.
onOpenChange(open: boolean) => void-Called when a collapsible step opens or closes.
Data attributeDescription
data-statusThe step's status.
data-stateopen 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>.

PropTypeDefaultDescription
donebooleanfalseThe dash cross-fades to a green check.
Data attributeDescription
data-donePresent 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.

PropTypeDefaultDescription
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>.

PropTypeDefaultDescription
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.

PropTypeDefaultDescription
expandedbooleanfalseFlips the label from {count} more to Show less.
countnumber-How many rows are hidden, shown while collapsed.
Data attributeDescription
data-expandedPresent when expanded.

PlanAddStep

The row that closes an editable list. Renders nothing outside edit mode. Forwards native <button> props.

PropTypeDefaultDescription
childrenReact.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.

FAQ#

Because a numbered column of filled discs reads as a diagram, and an approval card is something you scan in a second. The list is a semantic ordered list, so position and count are still announced by a screen reader; visually, the only mark a row carries is the 14px status glyph at its head. If a plan genuinely needs visible ordering, the group title and count carry it.