Decision Menu
A card-based menu for presenting AI-driven decisions to the user. Options are
selectable via click or keyboard — press 1–9 to jump
to an option by position, Enter to confirm, or Esc to
cancel. Clicking an already-selected option also confirms it.
Update the DNS record example.com to point to the new origin server?
import { useRef, useState } from "react";
import { Button } from "@cloudflare/kumo";
import { ArrowCounterClockwiseIcon } from "@phosphor-icons/react";
import { DecisionMenu } from "../kumo/decision-menu";
/** Full-featured decision menu with options, keyboard shortcuts, and footer. */
export function DecisionMenuDemo() {
const [status, setStatus] = useState<DemoStatus>("pending");
const [selected, setSelected] = useState("update");
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const handleSubmit = (value: string) => {
setSelected(value);
setStatus("applying");
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setStatus("done"), 1500);
};
const handleCancel = () => {
clearTimeout(timerRef.current);
setSelected("");
setStatus("cancelled");
};
const isPending = status === "pending";
return (
<div className="w-full max-w-lg">
<DecisionMenu onCancel={handleCancel}>
<DecisionMenu.Description>
<p>
Update the DNS record <strong>example.com</strong> to point to the
new origin server?
</p>
</DecisionMenu.Description>
<DecisionMenu.Actions
onSubmit={handleSubmit}
value={selected}
disabled={!isPending}
>
<DecisionMenu.Option label="Update existing record" value="update" />
<DecisionMenu.Option label="Create new record" value="create" />
<DecisionMenu.Option label="Skip this change" value="skip" />
</DecisionMenu.Actions>
<DecisionMenu.Footer>
{isPending ? (
<>
<DecisionMenu.ShortcutButton type="submit" shortcut="↵">
Confirm
</DecisionMenu.ShortcutButton>
<DecisionMenu.ShortcutButton type="cancel" shortcut="Esc">
Cancel
</DecisionMenu.ShortcutButton>
</>
) : (
<DecisionMenu.StatusIndicator
status={
status === "applying"
? "loading"
: status === "done"
? "done"
: status === "cancelled"
? "cancelled"
: "failed"
}
action={
status !== "applying" && (
<Button
variant="secondary"
size="sm"
shape="square"
icon={ArrowCounterClockwiseIcon}
aria-label="Retry"
onClick={() => {
clearTimeout(timerRef.current);
setStatus("pending");
setSelected("update");
}}
/>
)
}
/>
)}
</DecisionMenu.Footer>
</DecisionMenu>
</div>
);
} Status Indicators
After a decision is submitted, the footer transitions to a status indicator
showing the outcome. Four states are supported: loading,
done, cancelled, and failed. The failed
state supports an expandable description for error details.
done
cancelled
Working on it...
failed
import { DecisionMenu } from "../kumo/decision-menu";
/** All four status indicator states shown side by side. */
export function DecisionMenuStatusDemo() {
return (
<div className="w-full max-w-lg flex flex-col gap-3">
{STATUSES.map((status) => (
<DecisionMenu key={status}>
<DecisionMenu.StatusIndicator
status={status}
description={
status === "failed"
? "DNS validation failed: CNAME record conflicts with an existing A record for example.com. Remove the A record first, then retry."
: undefined
}
/>
</DecisionMenu>
))}
</div>
);
} Keyboard Controls
| Key | Action |
|---|---|
1–9 | Jump to option by position |
Enter | Confirm the selected option |
Esc | Cancel — deselects all options |
↑ ↓ | Navigate between options (native radio behavior) |
Installation
DecisionMenu is an AI block — a CLI-installed component that you own and can customize.
1. Initialize Kumo config (first time only)
npx @cloudflare/kumo init2. Install the block
npx @cloudflare/kumo add DecisionMenu3. Import from your local path
import { DecisionMenu } from "./components/kumo/decision-menu/decision-menu";API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "base" | "lg" | "base" | - |
| children | ReactNode | - | - |
| className | string | - | - |