# SureUI > Open source confirmation components for shadcn/ui: undo, hold to confirm, type to confirm and more. Built on Base UI. Installed with the shadcn CLI from the `@sureui` registry. --- # Introduction Open source confirmation components for shadcn/ui: undo, hold to confirm, type to confirm and more. Built on Base UI. Source: https://sureui.com/docs When every action opens an "Are you sure?" dialog, people stop reading and confirm on reflex. The one dialog that matters gets the same click as the fifty before it. SureUI has other ways to ask: undo, hold to confirm, type to confirm, a second click, or a dialog. Each component installs into your project from the `@sureui` registry, so the code is yours to change. ## Components ### On the control itself - [Confirm Button](https://sureui.com/llms/confirm-button.md) confirms on a click, a second click or a press and hold, with optional undo. - [Confirm Menu Item](https://sureui.com/llms/confirm-menu-item.md) brings the same gestures to dropdown and context menu items. - [Confirm Switch](https://sureui.com/llms/confirm-switch.md) moves right away and can be flipped back during the undo window. ### When it needs more room - [Type to Confirm](https://sureui.com/llms/type-to-confirm.md) unlocks only after the exact phrase is typed. - [Confirm Dialog](https://sureui.com/llms/confirm-dialog.md) puts a confirmation in an alert dialog, with an awaitable `useConfirm`. - [Confirm Popover](https://sureui.com/llms/confirm-popover.md) anchors a one-line confirmation to its trigger. - [Consequences](https://sureui.com/llms/consequences.md) lists what a confirmation will remove or change, with counts and names. ### Undo after the fact - [Undo Toast](https://sureui.com/llms/undo-toast.md) shows a toast with Undo and resolves once nobody undoes. - [Undoable](https://sureui.com/llms/undoable.md) collapses a removed row in place to a label and an Undo button. ### Moments with their own rules - [Unsaved Changes](https://sureui.com/llms/unsaved-changes.md) asks before unsaved changes are lost, when leaving a page or closing a dialog. - [Tool Approval](https://sureui.com/llms/tool-approval.md) approves or denies an AI SDK tool call with a gesture that matches its risk. To build your own trigger, like a card or a keyboard shortcut, use the hook every component is built on: [Confirmation Core](https://sureui.com/llms/confirmation-core.md). ## One contract Every component except Tool Approval takes the same `onConfirm`. Return a promise and the control stays pending until it settles. `onCancel` runs when someone backs out or presses Undo. Every control sets `data-state`, so you can style around it. ```tsx async function deleteProject() { await api.projects.delete(id) } Delete Delete Delete ``` --- # Installation Add SureUI to a shadcn/ui project with the shadcn CLI. Source: https://sureui.com/docs/installation ## Set up shadcn/ui SureUI installs into a shadcn/ui project on Base UI, the shadcn default. Projects on Radix aren't supported: the components use Base UI's `render` prop. If your project doesn't use shadcn yet, run: ```bash npx shadcn@latest init ``` ## Add components `@sureui` is in the shadcn registry index, so the CLI finds it without any setup. Each item includes the confirmation core, so install only the ones you use: ```bash npx shadcn@latest add @sureui/confirm-button ``` Each component page lists its own command, and blocks install the same way. ## Coding agents SureUI has an agent skill for Claude Code, Cursor, Codex and other agents that read skills. Add it with the skills CLI: ```bash npx skills add aidankmcalister/sureui ``` The skill reads your project with `shadcn info`, picks a control by how hard the action is to take back, and installs it with the shadcn CLI. Ask it to review your code and it lists destructive actions that run on one click. Its rules are in [`skills/sureui`](https://github.com/aidankmcalister/sureui/tree/main/skills/sureui), and [llms.txt](https://sureui.com/llms.txt) lists every page. ## Use them in a client component Every SureUI component takes functions like `onConfirm`, so in the Next.js App Router, render it from a file that starts with `"use client"`. A Server Component can't pass functions to it, the same as with `onClick` on a Button. The examples on these pages already start with `"use client"`. ## Mount the Toaster `undoToast` shows a shadcn toast, so it needs the `Toaster` in your root layout. Nothing else needs setup. ```tsx import { Toaster } from "@/components/ui/sonner" export default function RootLayout({ children }) { return ( {children} ) } ``` --- # Choosing a confirmation Match how risky an action is to the confirmation that asks for it. Source: https://sureui.com/docs/choosing-a-confirmation [Carbon](https://carbondesignsystem.com/patterns/common-actions/), [GitLab Pajamas](https://design.gitlab.com/patterns/destructive-actions), [Apple's Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/alerts) and [Nielsen Norman Group](https://www.nngroup.com/articles/confirmation-dialog/) all sort destructive actions into three tiers. The harder an action is to take back, the more effort its confirmation asks for. A routine action that asks for too much teaches people to confirm without reading. Describe the action to see the control that fits, try it, and copy its code. - `ConfirmButton` (can be undone right away; one thing; on a button): It runs at once and offers Undo, which beats a question for frequent actions. Install: `npx shadcn@latest add @sureui/confirm-button`. Docs: https://sureui.com/docs/confirm-button#undo ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonUndo() { return ( Delete ) } ``` - `ConfirmMenuItem` (can be undone right away; one thing; in a menu): It runs at once and offers Undo, which beats a question for frequent actions. Install: `npx shadcn@latest add @sureui/confirm-menu-item`. Docs: https://sureui.com/docs/confirm-menu-item#undo ```tsx "use client" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { ConfirmMenuItem } from "@/components/ui/sureui/confirm-menu-item" export default function ConfirmMenuItemUndo() { return ( }>Actions Archive ) } ``` - `Undoable` (can be undone right away; one thing; on a row in a list): The row collapses to Undo in place, so nothing needs asking first. Install: `npx shadcn@latest add @sureui/undoable`. Docs: https://sureui.com/docs/undoable ```tsx "use client" import * as React from "react" import { Button } from "@/components/ui/button" import { Undoable } from "@/components/ui/sureui/undoable" export default function UndoableDemo() { const [files, setFiles] = React.useState(["q3-report.pdf", "notes.md"]) return ( ) } ``` - `useConfirm` (in a select or radio group): The field keeps its old value until the dialog is confirmed, so cancelling changes nothing. Install: `npx shadcn@latest add @sureui/confirm-dialog`. Docs: https://sureui.com/docs/confirm-dialog#confirm-a-select-change ```tsx "use client" import * as React from "react" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { useConfirm } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogSelectChange() { const [role, setRole] = React.useState("Admin") const { confirm, dialog } = useConfirm() async function change(next: string | null) { if (!next || next === role) return const confirmed = await confirm({ title: `Make Ava Diaz a ${next}?`, description: `Ava is ${role} now.`, confirmLabel: `Make ${next}`, }) if (!confirmed) return setRole(next) changeRole(next) } return ( <> {dialog} ) } ``` - `useUnsavedChanges` (leaving unsaved edits): It asks before edits are lost, on navigation, on close and on reload. Install: `npx shadcn@latest add @sureui/unsaved-changes`. Docs: https://sureui.com/docs/unsaved-changes ```tsx "use client" import * as React from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { useUnsavedChanges } from "@/components/ui/sureui/unsaved-changes" export default function UnsavedChangesDemo() { const [page, setPage] = React.useState("General") const [name, setName] = React.useState("Acme") const { confirmLeave, dialog } = useUnsavedChanges({ when: name !== "Acme", onDiscard: () => setName("Acme"), }) async function open(to: string) { if (await confirmLeave()) setPage(to) } return (
{page === "General" ? ( setName(event.target.value)} /> ) : (

Billing settings

)} {dialog}
) } ``` - `ToolApproval` (can be undone right away; in an AI tool call): The "low" risk level matches how hard the action is to take back. Install: `npx shadcn@latest add @sureui/tool-approval`. Docs: https://sureui.com/docs/tool-approval#low-risk ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalLow() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` - `ConfirmButton` or `TypeToConfirm` (many things at once; on a button, in a menu or on a row in a list): Let the friction grow with the count: undo for a few, a second click for dozens, and the count typed for hundreds. Install: `npx shadcn@latest add @sureui/type-to-confirm`. Docs: https://sureui.com/docs/choosing-a-confirmation#many-at-once ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" import { TypeToConfirm } from "@/components/ui/sureui/type-to-confirm" export default function ChoosingByCount() { const count = 3 const label = `Delete ${count} issues` if (count > 100) { return ( ) } return ( 10 ? "click-again" : "click"} undo={count <= 10} variant="destructive" onConfirm={deleteIssues} > {label} ) } ``` - `ConfirmButton` (only your app can restore it later, from a trash or archive; one thing; on a button or on a row in a list): A second click in place is enough when it can be restored. Install: `npx shadcn@latest add @sureui/confirm-button`. Docs: https://sureui.com/docs/confirm-button#click-again ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonClickAgain() { return ( Delete branch ) } ``` - `ConfirmMenuItem` (only your app can restore it later, from a trash or archive; one thing; in a menu): A second click in place is enough when it can be restored. Install: `npx shadcn@latest add @sureui/confirm-menu-item`. Docs: https://sureui.com/docs/confirm-menu-item ```tsx "use client" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { ConfirmMenuItem } from "@/components/ui/sureui/confirm-menu-item" export default function ConfirmMenuItemDemo() { return ( }>Actions Rename Delete ) } ``` - `ToolApproval` (only your app can restore it later, from a trash or archive; in an AI tool call): The "medium" risk level matches how hard the action is to take back. Install: `npx shadcn@latest add @sureui/tool-approval`. Docs: https://sureui.com/docs/tool-approval#medium-risk ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalMedium() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` - `ConfirmDialog` (can't be undone; one thing; on a button, in a menu or on a row in a list): It can't be taken back, so show what goes and ask for the name. Install: `npx shadcn@latest add @sureui/confirm-dialog`. Docs: https://sureui.com/docs/type-to-confirm#in-a-dialog ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" export default function TypeToConfirmDialog() { return ( ) } ``` - `ToolApproval` (can't be undone; in an AI tool call): The "critical" risk level matches how hard the action is to take back. Install: `npx shadcn@latest add @sureui/tool-approval`. Docs: https://sureui.com/docs/tool-approval#critical-risk ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalCritical() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` | Risk | Actions like | Use | | ------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Low | Archiving a message, removing an item from a list. Common and easy to reverse. | Undo: [Confirm Button](https://sureui.com/llms/confirm-button.md) with `undo`, [Undo Toast](https://sureui.com/llms/undo-toast.md) or [Undoable](https://sureui.com/llms/undoable.md). | | Medium | Removing a member, deleting a branch, revoking a key. | A second click or a popover: [Confirm Button](https://sureui.com/llms/confirm-button.md) with `gesture="click-again"`, [Confirm Menu Item](https://sureui.com/llms/confirm-menu-item.md) or [Confirm Popover](https://sureui.com/llms/confirm-popover.md). A hold for dangerous one-offs. | | High | Deleting a project, a database or an account. Irreversible, with wide impact. | A dialog that lists what goes and asks for the name: [Confirm Dialog](https://sureui.com/llms/confirm-dialog.md) with `phrase`, or [Type to Confirm](https://sureui.com/llms/type-to-confirm.md). | ## Low risk Do it right away and offer Undo. NN/g and Apple both recommend undo over a confirmation for frequent actions: a question people answer many times a day stops being read, and Undo only costs a click when someone made a mistake. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ChoosingLow() { return ( Archive 3 conversations ) } ``` - [Confirm Button](https://sureui.com/llms/confirm-button.md) with `undo` when the button stays on screen. - [Undo Toast](https://sureui.com/llms/undo-toast.md) when the control goes away, like an email archived from its own view. - [Undoable](https://sureui.com/llms/undoable.md) when a row leaves a list or table. `onConfirm` runs when the window closes, so there is nothing to reverse on your server when someone presses Undo. ## Medium risk Ask for a second, deliberate step in place, without a modal. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ChoosingMedium() { return (
Remove member Hold to revoke key
) } ``` - [Confirm Button](https://sureui.com/llms/confirm-button.md) with `gesture="click-again"` arms on the first click and confirms on the second. - [Confirm Menu Item](https://sureui.com/llms/confirm-menu-item.md) does the same in dropdown and context menus. Click again is its default. - [Confirm Popover](https://sureui.com/llms/confirm-popover.md) when the action needs a sentence of context, like which pull requests will close. - `gesture="hold"` for a dangerous one-off, like revoking a production key, where a double click shouldn't count. Confirm Button, Confirm Menu Item, Confirm Popover and Confirm Dialog all take it. - `gesture="slide"` on touch screens, where dragging to the end is harder to do by accident than a tap. Two taps also confirm, for people who can't drag. Confirm Button, Confirm Popover and Confirm Dialog take it. ## High risk For an irreversible action with wide impact, show what will be lost and ask for the name of the thing. Typing the name shows people know which project they are deleting, not only that they meant to click. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" import { Consequences } from "@/components/ui/sureui/consequences" export default function ChoosingHigh() { return ( } confirmLabel="Delete acme-prod" variant="destructive" onConfirm={deleteProject} > ) } ``` - [Confirm Dialog](https://sureui.com/llms/type-to-confirm.md) with `phrase` and `consequences`. - [Type to Confirm](https://sureui.com/llms/type-to-confirm.md) inline, like in a settings page's danger zone. - [Consequences](https://sureui.com/llms/consequences.md) lists what goes with it, with counts and names. - `wait` on [Confirm Dialog](https://sureui.com/llms/confirm-dialog.md) when there's no name to type: a short countdown before confirm unlocks, so people read before they act. Keep this tier for the few actions that need it. ## Many at once When one action hits many things, let the friction grow with the count: undo for a few, a second click for dozens, and the count typed for hundreds. There's nothing extra to install. Pick the control from the count. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" import { TypeToConfirm } from "@/components/ui/sureui/type-to-confirm" export default function ChoosingByCount() { const count = 3 const label = `Delete ${count} issues` if (count > 100) { return ( ) } return ( 10 ? "click-again" : "click"} undo={count <= 10} variant="destructive" onConfirm={deleteIssues} > {label} ) } ``` ```bash npx shadcn@latest add @sureui/type-to-confirm ``` Type to Confirm installs Confirm Button with it. The [bulk actions block](/blocks#bulk-actions-01) shows the same idea in an issue list. ## AI agents [Tool Approval](https://sureui.com/llms/tool-approval.md) maps its `risk` levels onto the same tiers. Deny is one click at every level. | `risk` | Approve with | Tier | | ------------ | ---------------------------- | ------------------------------ | | `"low"` | A click, then an undo window | Low | | `"medium"` | A second click | Medium | | `"high"` | A press and hold | Medium, for dangerous one-offs | | `"critical"` | The typed `phrase` | High | ```tsx ``` ## Labels - Use a verb that says what happens: "Delete project", not "OK" or "Yes". The button should make sense without reading the question above it. - Name the thing and the count: "Delete 3 files" or "Delete acme-prod", not "Are you sure?" or "Delete selected". - Don't rely on red alone. Color doesn't reach everyone, so `variant="destructive"` goes with a label that says "Delete", never instead of it. - For frequent actions, skip the question and offer undo. ## Accessibility - An undo window is a time limit under [WCAG 2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable). It pauses while the pointer or focus is on the control and while the tab is hidden, and `undo` takes up to 60 seconds. For an action people may need longer to reconsider, keep a way to restore it afterwards, like a trash, as in the [file manager block](/blocks#file-manager-01). - A hold meets [WCAG 2.5.2 Pointer Cancellation](https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation) because letting go early cancels. - Keyboard users hold Space or Enter. Screen reader users can confirm a hold with two activations. See [Confirm Button](https://sureui.com/llms/confirm-button.md). --- # Confirm Button A button that runs its action on a click, a second click or a press and hold, with optional undo. Source: https://sureui.com/docs/confirm-button ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonDemo() { return ( Delete project ) } ``` ## Installation ```bash npx shadcn@latest add @sureui/confirm-button ``` ## Usage ```tsx import { ConfirmButton } from "@/components/ui/sureui/confirm-button" Delete project ``` `gesture` picks how people confirm, and `undo` adds a few seconds to take it back. ## Examples ### Undo The button turns into Undo right away, and `onConfirm` waits until the window closes. Undo calls `onCancel` instead. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonUndo() { return ( Delete ) } ``` The window lasts 5 seconds, or the milliseconds you pass to `undo` between 4 and 60 seconds, and pauses while the pointer or focus is on the button. Unmounting during the window drops the action without calling either handler. With `undo="manual"`, Undo stays until people move on: a press outside the button or focus moving elsewhere runs `onConfirm`. There is no fill, since nothing is counting down. ### Click again The first click arms the button and shows `confirmLabel`. The second click confirms. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonClickAgain() { return ( Delete branch ) } ``` The button disarms after `timeout` or when it loses focus. ### Hold Press and hold until the fill completes, and it confirms. Letting go early cancels. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonHold() { return ( Hold to revoke ) } ``` Moving off the button or losing focus also cancels. ### Slide `gesture="slide"` confirms when people drag across the button and let go at the end. Letting go early slides it back and cancels, and scrolling the page up or down still works. Two clicks or taps, or two presses of Enter or Space, also confirm, like click again, so nobody has to drag. ```tsx "use client" import { ChevronsRightIcon } from "lucide-react" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonSlide() { return ( Slide to delete ) } ``` ### Pending Return a promise from `onConfirm` and a ring in the button's color runs around it until it settles. The ring sits 3 pixels outside the button, so a container that clips its overflow cuts it off. Pass `pendingLabel` to say what is happening while it runs. If the promise rejects, the button goes back to idle. Set `pendingIndicator` to `"spinner"` to put a spinner in front of the label, or `"pulse"` to fade the button in and out. Set `pendingDelay` in milliseconds to wait before showing any of them, so fast actions don't flash. The button keeps the width of its widest label, so it doesn't jump. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonPending() { return ( Deploy ) } ``` ### Success Pass `successLabel` to show it for 1.5 seconds after the promise resolves. A rejection skips it and shows `errorLabel` instead, if you set one. ```tsx "use client" import { CheckIcon } from "lucide-react" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonSuccess() { return ( Published } onConfirm={publish} > Publish ) } ``` ### Errors By default, when `onConfirm` throws or rejects, the button goes back to idle and the error reaches your app unchanged. Pass `onConfirmError` to receive it instead, and `errorLabel` to show the failure on the button until the next activation, which tries again. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonErrors() { return ( Publish ) } ``` The error label is announced in the polite live region. Change the text with `announcements.error`. ### Arm delay `armDelay` ignores activation for that many milliseconds after the button mounts or arms, so the second click of a double click can't pass through. It helps most inside a dialog or popover, where the button appears under the pointer. Double-click Show below: the second click lands on the new button and does nothing. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonArmDelay() { return ( Delete project ) } ``` ### Wait `wait` keeps the button disabled for that many milliseconds after it appears, counting down on the button ("Wait 3s"), so people read before they confirm. Screen readers hear "Available in 3 seconds" once. Change the text with `waitLabel` and `announcements.wait`. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonWait() { return ( Delete project ) } ``` ### Labels `confirmLabel` shows while armed (default "Click again"; screen readers hear "Click again to confirm") and `undoLabel` during the undo window. ```tsx "use client" import { ConfirmButton } from "@/components/ui/sureui/confirm-button" export default function ConfirmButtonLabels() { return (
Archive all Remove
) } ``` ## API reference ### ConfirmButton Extends the shadcn [`Button`](https://ui.shadcn.com/docs/components/base/button). The table lists the props it adds. | Prop | Type | Default | | ------------------ | ---------------------------------------------------------------------- | ------------------------------------------ | | `...buttonProps` | [`Button`](https://ui.shadcn.com/docs/components/base/button) props | — | | `onConfirm` | `() => void \| Promise` | required | | `onCancel` | `() => void` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `gesture` | `"click" \| "click-again" \| "hold" \| "slide"` | `"click"` | | `undo` | `boolean \| number \| "manual"` | — | | `pauseUndoOnHover` | `boolean` | `true` | | `pauseUndoOnFocus` | `boolean` | `true` | | `confirmLabel` | `ReactNode` | `"Click again"`, or `"Confirm"` for a hold | | `undoLabel` | `ReactNode` | `"Undo"` | | `errorLabel` | `ReactNode` | — | | `armDelay` | `number` | `0` | | `wait` | `number` | `0` | | `waitLabel` | `(seconds: number) => ReactNode` | ``(s) => `Wait ${s}s` `` | | `pendingIndicator` | `"ring" \| "spinner" \| "pulse"` | `"ring"` | | `pendingLabel` | `ReactNode` | — | | `pendingDelay` | `number` | `0` | | `successLabel` | `ReactNode` | — | | `timeout` | `number` | `3000` | | `duration` | `number` | `1200` | | `holdFallback` | `"click-again" \| "none"` | `"click-again"` | | `announcements` | `{ armed?, hold?, slide?, fallback?, wait?, undo?, error?, success? }` | — | ### Data attributes The button sets `data-state`, so you can style around it. After `onConfirm` fails, it also sets `data-error` until the next activation. While its pending indicator shows, it also sets `data-pending`. | Value | When | | --------- | --------------------------------------------------------------------------------- | | `idle` | Waiting for input. | | `armed` | Clicked once with `click-again`, or a hold that fell back to a second activation. | | `holding` | Being held with `gesture="hold"`. | | `undo` | The undo window is open. | | `pending` | `onConfirm` returned a promise that hasn't settled. | ## Accessibility - Key repeat is ignored, so holding Enter can't arm and confirm at once. - Each state change is announced in a polite live region. Replace the English text with `announcements`. - Keyboard users hold Space or Enter, and letting go early cancels. Screen readers send a click that can't be held, so a click with no press arms the button and a second one confirms. Set `holdFallback="none"` to turn this off. - With `prefers-reduced-motion`, fills jump to their end state instead of moving, the ring and the spinner hold still instead of running, and `pulse` dims the button instead of pulsing. - While `onConfirm` is pending, the button sets `aria-busy` and keeps focus. `successLabel` is announced in the same polite live region as the other states. ### WCAG - [2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable): `undo="manual"` meets it, since Undo has no time limit. A numeric `undo` stops at 60 seconds. - `wait` isn't a time limit under 2.2.1: it only delays when confirm unlocks, and never ends anything or drops the action. - [2.5.2 Pointer Cancellation](https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation): `gesture="hold"` meets it, because releasing early or moving off the button cancels. `click` and `click-again` run on the up event. - [2.5.7 Dragging Movements](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements): `gesture="slide"` meets it, because two clicks confirm without dragging. --- # Confirm Dialog An alert dialog that asks before an action runs, with a confirm button, a hold or a typed phrase, and an awaitable confirm(). Source: https://sureui.com/docs/confirm-dialog ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogDemo() { return ( ) } ``` ## Installation ```bash npx shadcn@latest add @sureui/confirm-dialog ``` ## Usage ```tsx import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" ``` If `onConfirm` returns a promise, the dialog stays open until it settles. `variant` styles only the confirm button. The trigger is your own element, so an outline trigger can open a destructive confirm step, as above. ## Examples ### Typed phrase Pass `phrase` to require typing it before the confirm button unlocks, as on [Type to Confirm](https://sureui.com/llms/type-to-confirm.md). ### Choices Each item in `choices` adds a checkbox that doesn't block confirming. `onConfirm` receives their values by `name`, as they were when the person confirmed. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogChoices() { return ( ) } ``` ### Alternative `alternative` adds a softer action, such as archiving instead of deleting. It gets its own full-width row under Cancel and the confirm button, so the gentler choice is the easiest to reach. It doesn't need the phrase, and `onCancel` doesn't run. If `onSelect` returns a promise, the dialog stays open until it settles. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogAlternative() { return ( ) } ``` ### Initial focus Focus starts on Cancel. `initialFocus="confirm"` starts on the confirm button, for actions that are easy to reverse, and `"none"` focuses the dialog itself. With `phrase`, focus starts in the first field, and `"confirm"` does the same, since the confirm button stays disabled until the phrase matches. Keep `"cancel"` for anything that deletes data or can't be undone. The WAI-ARIA [dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) advises focusing the least destructive action when the step isn't easily reversed. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogInitialFocus() { return ( ) } ``` ### Hold `gesture` works as on [Confirm Button](https://sureui.com/llms/confirm-button.md), here with `"hold"`. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogHold() { return ( ) } ``` ### Arm delay `armDelay` ignores the confirm button for that many milliseconds after the dialog opens, so a double click on the trigger can't pass through to it. It works as on [Confirm Popover](https://sureui.com/llms/confirm-popover.md). ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogArmDelay() { return ( ) } ``` ### Wait `wait` keeps the confirm button disabled for that many milliseconds after the dialog opens, with a countdown on it, so people read the dialog before confirming. It starts again each time the dialog opens, and works as on [Confirm Button](https://sureui.com/llms/confirm-button.md). With a `phrase`, typing already slows people down, so `wait` doesn't apply. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogWait() { return ( ) } ``` ### Errors With `onConfirmError`, a failed `onConfirm` keeps the dialog open and the error goes to your handler. `errorLabel` replaces the confirm label until the next activation, which tries again. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogErrors() { return ( ) } ``` ### Consequences Pass a [Consequences](https://sureui.com/llms/consequences.md) list to show what the action removes. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" import { Consequences } from "@/components/ui/sureui/consequences" export default function ConfirmDialogConsequences() { return ( } confirmLabel="Delete environment" variant="destructive" onConfirm={deleteEnvironment} > ) } ``` ### Awaiting the answer `useConfirm()` returns a `confirm()` to await in a handler and a `dialog` to render once. Use it when confirming removes the button that opened the dialog, like a row that turns into a badge: `dialog` stays mounted, so it closes normally instead of disappearing. ```tsx "use client" import { Button } from "@/components/ui/button" import { useConfirm } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogUseConfirm() { const { confirm, dialog } = useConfirm() async function handleDiscard() { const discard = await confirm({ title: "Discard this draft?", description: "Your edits to the Q3 roadmap won't be saved.", confirmLabel: "Discard", variant: "destructive", }) if (discard) discardDraft() } return ( <> {dialog} ) } ``` ### Confirm a select change A select or radio group can ask before its new value applies. Keep the field on the old value, await `confirm()`, and only set the new value when it resolves `true`. Cancelling changes nothing. ```tsx "use client" import * as React from "react" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { useConfirm } from "@/components/ui/sureui/confirm-dialog" export default function ConfirmDialogSelectChange() { const [role, setRole] = React.useState("Admin") const { confirm, dialog } = useConfirm() async function change(next: string | null) { if (!next || next === role) return const confirmed = await confirm({ title: `Make Ava Diaz a ${next}?`, description: `Ava is ${role} now.`, confirmLabel: `Make ${next}`, }) if (!confirmed) return setRole(next) changeRole(next) } return ( <> {dialog} ) } ``` ## API reference ### ConfirmDialog For a way back after it closes, use [Undo Toast](https://sureui.com/llms/undo-toast.md). | Prop | Type | Default | | ------------------ | ----------------------------------------------------------------------- | ----------------------------------------- | | `title` | `ReactNode` | required | | `children` | `ReactElement` | required | | `onConfirm` | `(choices: Record) => void \| Promise` | required | | `onCancel` | `() => void` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `description` | `ReactNode` | — | | `consequences` | `ReactNode` | — | | `confirmLabel` | `ReactNode` | `"Confirm"` | | `cancelLabel` | `ReactNode` | `"Cancel"` | | `errorLabel` | `ReactNode` | — | | `variant` | [`Button`](https://ui.shadcn.com/docs/components/base/button) `variant` | `"default"` | | `initialFocus` | `"cancel" \| "confirm" \| "none"` | `"cancel"`, the first field with `phrase` | | `alternative` | `{ label: ReactNode, onSelect: () => void \| Promise }` | — | | `gesture` | `"click" \| "click-again" \| "hold" \| "slide"` | `"click"` | | `armDelay` | `number` | `0` | | `wait` | `number` | `0` | | `waitLabel` | `(seconds: number) => ReactNode` | ``(s) => `Wait ${s}s` `` | | `timeout` | `number` | `3000` | | `duration` | `number` | `1200` | | `holdFallback` | `"click-again" \| "none"` | `"click-again"` | | `phrase` | `string \| string[]` | — | | `caseSensitive` | `boolean` | `true` | | `trim` | `boolean` | `false` | | `acknowledgements` | `string[]` | — | | `choices` | `{ name: string, label: ReactNode, defaultChecked?: boolean }[]` | — | | `announcements` | `{ armed?, hold?, slide?, fallback?, wait?, match?, error? }` | — | ### useConfirm() Takes no arguments. | Name | Type | Description | | ---------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `confirm` | `(options: ConfirmOptions) => Promise` | Opens the dialog. Resolves `false` on cancel, after `alternative`, when another `confirm()` replaces it, or on unmount. | | `dialog` | `ReactElement` | The dialog. Render it once. | | `ConfirmOptions` | `object` | `ConfirmDialog` props without `children` and `onCancel`. `onConfirm` is optional. | ## Accessibility - The dialog has the `alertdialog` role, named by `title` and described by `description`. - Focus moves into the dialog when it opens, to Cancel by default, and back to the trigger when it closes. - While `onConfirm` is pending, Cancel and Escape do nothing and the confirm button keeps focus. The same holds for the alternative's button while `onSelect` is pending. - With several phrases, each field has its own label and announces its own match. ### WCAG - [2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable): the dialog has no time limit. `timeout` only disarms `click-again`, and never runs or drops the action. - `wait` isn't a time limit under 2.2.1: it only delays when confirm unlocks, and never ends anything or drops the action. - [2.5.2 Pointer Cancellation](https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation): `gesture="hold"` meets it, because releasing early or moving off the button cancels. --- # Confirm Menu Item A dropdown or context menu item that asks for a second click or a press and hold before it runs, with optional undo. Source: https://sureui.com/docs/confirm-menu-item ```tsx "use client" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { ConfirmMenuItem } from "@/components/ui/sureui/confirm-menu-item" export default function ConfirmMenuItemDemo() { return ( }>Actions Rename Delete ) } ``` ## Installation ```bash npx shadcn@latest add @sureui/confirm-menu-item ``` ## Usage ```tsx import { DropdownMenuContent } from "@/components/ui/dropdown-menu" import { ConfirmMenuItem } from "@/components/ui/sureui/confirm-menu-item" Delete ``` The first click arms the item and the second confirms. The menu stays open until the action commits. ## Examples ### Hold `gesture="hold"` works as on [Confirm Button](https://sureui.com/llms/confirm-button.md). ```tsx "use client" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { ConfirmMenuItem } from "@/components/ui/sureui/confirm-menu-item" export default function ConfirmMenuItemHold() { return ( }>Actions Hold to revoke ) } ``` ### Undo With `undo`, the item turns into Undo, and pressing it cancels and closes the menu. Closing the menu during the window commits the action, since the item unmounts with the menu. With `undo="manual"`, Undo stays until people move to another item, press outside it or close the menu. ```tsx "use client" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { ConfirmMenuItem } from "@/components/ui/sureui/confirm-menu-item" export default function ConfirmMenuItemUndo() { return ( }>Actions Archive ) } ``` ### Errors `onConfirmError`, `errorLabel` and `armDelay` work as on [Confirm Button](https://sureui.com/llms/confirm-button.md). A failed item keeps the menu open and shows `errorLabel`, so the next activation tries again. ```tsx "use client" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { ConfirmMenuItem } from "@/components/ui/sureui/confirm-menu-item" export default function ConfirmMenuItemErrors() { return ( }>Actions Delete ) } ``` ### Context menu Set `menu="context"` inside a `ContextMenuContent`. ```tsx "use client" import { ContextMenu, ContextMenuContent, ContextMenuTrigger, } from "@/components/ui/context-menu" import { ConfirmMenuItem } from "@/components/ui/sureui/confirm-menu-item" export default function ConfirmMenuItemContextMenu() { return ( Right-click or long press here Delete ) } ``` ## API reference ### ConfirmMenuItem Extends the shadcn [`DropdownMenuItem`](https://ui.shadcn.com/docs/components/base/dropdown-menu), except `closeOnClick`. The table lists the props it adds. | Prop | Type | Default | | ------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------ | | `...menuItemProps` | [`DropdownMenuItem`](https://ui.shadcn.com/docs/components/base/dropdown-menu) props | — | | `onConfirm` | `() => void \| Promise` | required | | `onCancel` | `() => void` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `gesture` | `"click" \| "click-again" \| "hold"` | `"click-again"` | | `menu` | `"dropdown" \| "context"` | `"dropdown"` | | `closeOnConfirm` | `boolean` | `true` | | `undo` | `boolean \| number \| "manual"` | — | | `pauseUndoOnHover` | `boolean` | `true` | | `pauseUndoOnFocus` | `boolean` | `true` | | `confirmLabel` | `ReactNode` | `"Click again"`, or `"Confirm"` for a hold | | `undoLabel` | `ReactNode` | `"Undo"` | | `errorLabel` | `ReactNode` | — | | `armDelay` | `number` | `0` | | `timeout` | `number` | `3000` | | `duration` | `number` | `1200` | | `holdFallback` | `"click-again" \| "none"` | `"click-again"` | | `announcements` | `{ armed?, hold?, fallback?, undo?, error? }` | — | ### Data attributes The item sets the same `data-state` values and `data-error` as [Confirm Button](https://sureui.com/llms/confirm-button.md). ## Accessibility - Moving the highlight to another item disarms it. - Key repeat is ignored, so holding Enter can't arm and confirm at once. - Each state change is announced in a polite live region. Change the text with `announcements`. ### WCAG - [2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable): `undo="manual"` meets it, since Undo has no time limit. - [2.5.2 Pointer Cancellation](https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation): `gesture="hold"` meets it, because releasing early or moving off the item cancels. --- # Confirm Popover A popover anchored to its trigger with one line about the action and a confirm button. Source: https://sureui.com/docs/confirm-popover ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmPopover } from "@/components/ui/sureui/confirm-popover" export default function ConfirmPopoverDemo() { return ( ) } ``` ## Installation ```bash npx shadcn@latest add @sureui/confirm-popover ``` ## Usage ```tsx import { Button } from "@/components/ui/button" import { ConfirmPopover } from "@/components/ui/sureui/confirm-popover" ``` Cancel, Escape, a click outside or tabbing out close it and call `onCancel`. `variant` styles only the confirm button. The trigger is your own element, so an outline trigger can open a destructive confirm step, as above. ## Examples ### Hold `gesture` works as on [Confirm Button](https://sureui.com/llms/confirm-button.md), here with `"hold"`. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmPopover } from "@/components/ui/sureui/confirm-popover" export default function ConfirmPopoverHold() { return ( ) } ``` ### Title Add a `title` to show a heading above the description. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmPopover } from "@/components/ui/sureui/confirm-popover" export default function ConfirmPopoverTitle() { return ( ) } ``` ### Placement `side` picks where the popover opens, and `align` lines it up with the trigger. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmPopover } from "@/components/ui/sureui/confirm-popover" export default function ConfirmPopoverPlacement() { return ( ) } ``` ### Arm delay `armDelay` ignores the confirm button for that many milliseconds after the popover opens, so a double click on the trigger can't pass through to it. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmPopover } from "@/components/ui/sureui/confirm-popover" export default function ConfirmPopoverArmDelay() { return ( ) } ``` ### Wait `wait` keeps the confirm button disabled for that many milliseconds after the popover opens, with a countdown on it. It works as on [Confirm Button](https://sureui.com/llms/confirm-button.md). ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmPopover } from "@/components/ui/sureui/confirm-popover" export default function ConfirmPopoverWait() { return ( ) } ``` ### Errors With `onConfirmError`, a failed `onConfirm` keeps the popover open and the error goes to your handler. `errorLabel` replaces the confirm label until the next activation, which tries again. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmPopover } from "@/components/ui/sureui/confirm-popover" export default function ConfirmPopoverErrors() { return ( ) } ``` ### Initial focus Focus starts on the confirm button, so a keyboard user can confirm a quick action with one more key. `initialFocus="cancel"` starts on Cancel instead, and `"none"` focuses the popover itself. For an action that deletes data or can't be undone, the WAI-ARIA [dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) advises focusing the least destructive action. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmPopover } from "@/components/ui/sureui/confirm-popover" export default function ConfirmPopoverInitialFocus() { return ( ) } ``` ## API reference ### ConfirmPopover It has no `undo`, since it closes when the action commits. Use [Undo Toast](https://sureui.com/llms/undo-toast.md) for a way back. | Prop | Type | Default | | ---------------- | -------------------------------------------------------------------------- | ------------------------ | | `description` | `ReactNode` | required | | `children` | `ReactElement` | required | | `onConfirm` | `() => void \| Promise` | required | | `onCancel` | `() => void` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `title` | `ReactNode` | — | | `confirmLabel` | `ReactNode` | `"Confirm"` | | `errorLabel` | `ReactNode` | — | | `cancelLabel` | `ReactNode` | `"Cancel"` | | `initialFocus` | `"confirm" \| "cancel" \| "none"` | `"confirm"` | | `variant` | [`Button`](https://ui.shadcn.com/docs/components/base/button) `variant` | `"default"` | | `side` | `"top" \| "bottom" \| "left" \| "right" \| "inline-start" \| "inline-end"` | `"bottom"` | | `align` | `"start" \| "center" \| "end"` | `"center"` | | `open` | `boolean` | — | | `onOpenChange` | `(open: boolean) => void` | — | | `gesture` | `"click" \| "click-again" \| "hold" \| "slide"` | `"click"` | | `armDelay` | `number` | `0` | | `wait` | `number` | `0` | | `waitLabel` | `(seconds: number) => ReactNode` | ``(s) => `Wait ${s}s` `` | | `timeout` | `number` | `3000` | | `duration` | `number` | `1200` | | `holdFallback` | `"click-again" \| "none"` | `"click-again"` | | `announcements` | `{ armed?, hold?, slide?, fallback?, wait?, error? }` | — | ## Accessibility - The popover is named by `title`, or by `description` when there's no title. - Focus moves to the confirm button when it opens, or where `initialFocus` says, and back to the trigger when it closes. - While `onConfirm` is pending, the popover stays open and the confirm button keeps focus. ### WCAG - [2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable): the popover has no time limit. `timeout` only disarms `click-again`, and never runs or drops the action. - `wait` isn't a time limit under 2.2.1: it only delays when confirm unlocks, and never ends anything or drops the action. - [2.5.2 Pointer Cancellation](https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation): `gesture="hold"` meets it, because releasing early or moving off the button cancels. --- # Confirm Switch A switch that moves right away and can be flipped back during the undo window. Source: https://sureui.com/docs/confirm-switch ```tsx "use client" import { ConfirmSwitch } from "@/components/ui/sureui/confirm-switch" export default function ConfirmSwitchDemo() { return } ``` ## Installation ```bash npx shadcn@latest add @sureui/confirm-switch ``` ## Usage ```tsx import { ConfirmSwitch } from "@/components/ui/sureui/confirm-switch" ``` The switch moves as soon as it's flipped, and a small timer in its thumb empties while the undo window runs. Flipping it again during the window puts it back and calls `onCancel`. When the window ends, `onConfirm` runs with the new value and `onCheckedChange` follows. If `onConfirm` returns a promise, the switch stays pending until it settles. If it fails, the switch goes back. ## Examples ### One direction `confirmWhen="off"` only gives turning off an undo window. Turning on runs `onConfirm` right away. `confirmWhen="on"` does the opposite. ```tsx "use client" import { ConfirmSwitch } from "@/components/ui/sureui/confirm-switch" export default function ConfirmSwitchConfirmWhen() { return ( ) } ``` ### Ring `undoIndicator="ring"` draws the timer as a ring around the switch instead of in the thumb. ```tsx "use client" import { ConfirmSwitch } from "@/components/ui/sureui/confirm-switch" export default function ConfirmSwitchRing() { return ( ) } ``` ## API reference ### ConfirmSwitch Extends the shadcn [`Switch`](https://ui.shadcn.com/docs/components/base/switch). The table lists the props it adds or changes. | Prop | Type | Default | | ------------------ | ------------------------------------------------ | --------- | | `onConfirm` | `(checked: boolean) => void \| Promise` | required | | `onCancel` | `() => void` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `onCheckedChange` | `(checked: boolean) => void` | — | | `confirmWhen` | `"on" \| "off" \| "both"` | `"both"` | | `undoIndicator` | `"thumb" \| "ring"` | `"thumb"` | | `undo` | `boolean \| number \| "manual"` | `true` | | `pauseUndoOnHover` | `boolean` | `true` | | `pauseUndoOnFocus` | `boolean` | `true` | | `announcements` | `{ undo?, error? }` | — | `onCheckedChange` runs only once a change is confirmed, not when the switch first moves. ## Accessibility - It's a native `button` with the `switch` role, so Space and Enter flip it. - `aria-checked` follows the switch's position, including during the undo window. - The undo window and a failure are announced in a polite live region. Change the text with `announcements`. ### WCAG - [2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable): `undo="manual"` meets it, since Undo has no time limit. A numeric `undo` stops at 60 seconds. --- # Consequences A list of what a confirmation will remove, with counts and names. Source: https://sureui.com/docs/consequences ```tsx "use client" import { Consequences } from "@/components/ui/sureui/consequences" export default function ConsequencesDemo() { return ( ) } ``` ## Installation ```bash npx shadcn@latest add @sureui/consequences ``` ## Usage ```tsx import { Consequences } from "@/components/ui/sureui/consequences" ``` [Type to Confirm](https://sureui.com/llms/type-to-confirm.md) and [Confirm Dialog](https://sureui.com/llms/confirm-dialog.md) take a list in their `consequences` prop. ## Examples ### Long lists Names past `limit` collapse into an "and N more" button. The count comes from the names, or from `count` when there are none to list. ```tsx "use client" import { Consequences } from "@/components/ui/sureui/consequences" export default function ConsequencesLongLists() { return ( ) } ``` If you only have some of the names, pass the total as `count` and "and N more" includes the rest. Set `expandable={false}` to keep it as text. ### Subject `subject` names the thing being deleted, with `subjectDescription` for a line of facts under it, so people can check it's the right one. ```tsx "use client" import { Consequences } from "@/components/ui/sureui/consequences" export default function ConsequencesSubject() { return ( ) } ``` ### Changes For a change rather than a loss, give an item `from` and `to`. The row shows "Pro → Free" where the count would be, and screen readers hear "Plan, from Pro to Free". Leave out `from` to show only the new value. ```tsx "use client" import { Consequences } from "@/components/ui/sureui/consequences" export default function ConsequencesChanges() { return ( ) } ``` ### Destructive `variant="destructive"` tints the whole list. ```tsx "use client" import { Consequences } from "@/components/ui/sureui/consequences" export default function ConsequencesDestructive() { return ( ) } ``` ## API reference ### Consequences Each entry in `items` takes the props of `ConsequencesItem`. | Prop | Type | Default | | -------------------- | ---------------------------- | --------------------- | | `...divProps` | `ComponentProps<"div">` | — | | `subject` | `ReactNode` | — | | `subjectDescription` | `ReactNode` | — | | `items` | `Consequence[]` | — | | `children` | `ReactNode` | — | | `title` | `ReactNode` | — | | `variant` | `"default" \| "destructive"` | `"default"` | | `limit` | `number` | `3` | | `expandable` | `boolean` | `true` | | `moreLabel` | `(hidden: number) => string` | `"and {hidden} more"` | | `lessLabel` | `string` | `"Show less"` | ### ConsequencesItem It uses the `limit`, `expandable`, `moreLabel` and `lessLabel` set on `Consequences`. | Prop | Type | Default | | ------------- | ---------------------- | -------- | | `...liProps` | `ComponentProps<"li">` | — | | `label` | `ReactNode` | required | | `count` | `number` | — | | `from` | `ReactNode` | — | | `to` | `ReactNode` | — | | `names` | `string[]` | `[]` | | `icon` | `ReactNode` | — | | `description` | `ReactNode` | — | ## Accessibility - It renders a list, named by `title` when there is one. - The "and N more" button sets `aria-expanded` and doesn't submit a surrounding form. - A change row reads as "Plan, from Pro to Free": the arrow is hidden from screen readers. --- # Tool Approval Approve or deny an AI SDK tool call with a gesture that matches its risk. Source: https://sureui.com/docs/tool-approval ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalDemo() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` ## Installation ```bash npx shadcn@latest add @sureui/tool-approval ``` ## Usage Mark a tool as needing approval on the server ([AI SDK tool approval](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-tool-usage)): ```tsx import { ToolLoopAgent } from "ai" const agent = new ToolLoopAgent({ model, tools: { deleteProject }, toolApproval: { deleteProject: "user-approval" }, }) ``` Render `ToolApproval` for its tool part and pass `addToolApprovalResponse` from [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat): ```tsx import { useChat } from "@ai-sdk/react" import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai" import { ToolApproval } from "@/components/ui/sureui/tool-approval" const { messages, addToolApprovalResponse } = useChat({ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses, }) { message.parts.map((part) => part.type === "tool-deleteProject" ? ( ) : null ) } ``` It shows Approve and Deny while the part is `approval-requested`, and the outcome once it's answered. `sendAutomaticallyWhen` continues the conversation after you respond. ## Examples ### Low risk Approve runs after an undo window, so a misclick can be taken back before the agent hears about it. ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalLow() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` ### Medium risk The default. Approve arms on the first click and responds on the second. ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalMedium() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` ### High risk Approve responds once the hold completes. Letting go early cancels. ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalHigh() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` ### Critical risk Approve unlocks once `phrase` is typed, like a project or repository name. ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalCritical() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` Deny is one click at every level. ### Arm delay `armDelay` ignores Approve for that many milliseconds after it appears and after it arms, so a click meant for something that was there a moment ago doesn't approve the call. ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalArmDelay() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` ### Errors If `onRespond` throws or rejects, the buttons unlock so you can answer again. With `onConfirmError`, the error goes to your handler, and `errorLabel` replaces the Approve label until the next attempt. ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalErrors() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { await addToolApprovalResponse(response) setPart({ state: "approval-responded", approval: response }) }} onConfirmError={showError} /> ) } ``` ### Batch `ToolApprovalBatch` answers every pending call at once. Its Approve uses the gesture of the riskiest call, and it calls `onRespond` once per call. Pass `risk` as a function to grade each part. Parts that aren't `approval-requested` are skipped. ```tsx "use client" import * as React from "react" import { ToolApprovalBatch, type ToolApprovalPart, type ToolApprovalRisk, } from "@/components/ui/sureui/tool-approval" type Call = ToolApprovalPart & { risk: ToolApprovalRisk } export default function ToolApprovalBatchExample() { const [calls, setCalls] = React.useState([ { state: "approval-requested", approval: { id: "call_1" }, risk: "low" }, { state: "approval-requested", approval: { id: "call_2" }, risk: "medium" }, { state: "approval-requested", approval: { id: "call_3" }, risk: "high" }, ]) return ( call.risk} onRespond={(response) => { setCalls((current) => current.map((call) => call.approval?.id === response.id ? { ...call, state: "approval-responded", approval: response } : call ) ) addToolApprovalResponse(response) }} /> ) } ``` ```tsx import { isToolUIPart } from "ai" import { ToolApprovalBatch } from "@/components/ui/sureui/tool-approval" (part.type === "tool-deleteProject" ? "high" : "low")} onRespond={addToolApprovalResponse} /> ``` ### Scope Pass `scopes` to let people choose how long an answer holds: once, this session or always. The first entry starts selected, and `onRespond` receives it as `scope` with Approve and Deny. SureUI doesn't store permissions: keep the answer for the session or save it, and skip `ToolApproval` for that tool next time. `addToolApprovalResponse` ignores `scope`, so you can pass it on unchanged. ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalScopes() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` ### Note Pass `note` to add a field above Approve and Deny. Whatever people type goes out as `reason` with either answer. `addToolApprovalResponse` passes it to the model, so the agent can read "only the staging table" or "use the archive tool instead". The outcome shows the note after the answer. ```tsx "use client" import * as React from "react" import { ToolApproval, type ToolApprovalPart, } from "@/components/ui/sureui/tool-approval" export default function ToolApprovalNote() { const [part, setPart] = React.useState({ state: "approval-requested", approval: { id: "approval_1" }, }) return ( { setPart({ state: "approval-responded", approval: response }) addToolApprovalResponse(response) }} /> ) } ``` ## API reference ### ToolApproval `onRespond` takes the same argument as `addToolApprovalResponse`, and `part` takes any AI SDK tool part. | Prop | Type | Default | | ---------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `part` | `{ state, approval?: { id, approved?, reason? } }` | required | | `onRespond` | `({ id, approved, reason?, scope? }) => void \| PromiseLike` | required | | `risk` | `"low" \| "medium" \| "high" \| "critical"` | `"medium"` | | `phrase` | `string` | required with `"critical"` | | `approveLabel` | `ReactNode` | `"Approve"`, or `"Hold to approve"` for `"high"` | | `denyLabel` | `ReactNode` | `"Deny"` | | `approvedLabel` | `ReactNode` | `"Approved"` | | `deniedLabel` | `ReactNode` | `"Denied"` | | `errorLabel` | `ReactNode` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `undo` | `boolean \| number \| "manual"` | `true` | | `timeout` | `number` | `3000` | | `duration` | `number` | `1200` | | `armDelay` | `number` | `0` | | `scopes` | `("once" \| "session" \| "always")[]` | — | | `scopeLabels` | `{ group?, once?, session?, always? }` | `{ group: "Remember", once: "Once", session: "This session", always: "Always" }` | | `note` | `boolean` | `false` | | `noteLabel` | `string` | `"Add a note for the agent"` | | `className` | `string` | — | `undo` applies to `"low"`, `timeout` to `"medium"` and `duration` to `"high"`. `scope` is only sent when `scopes` is set. ### ToolApprovalBatch Takes the same props as `ToolApproval`, with `parts` in place of `part`. | Prop | Type | Default | | ---------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `parts` | `ToolApprovalPart[]` | required | | `onRespond` | `({ id, approved, reason?, scope? }) => void \| PromiseLike` | required | | `risk` | `ToolApprovalRisk \| (part) => ToolApprovalRisk` | `"medium"` | | `phrase` | `string` | `"approve all"` | | `approveLabel` | `ReactNode` | `"Approve all"`, or `"Hold to approve all"` for `"high"` | | `denyLabel` | `ReactNode` | `"Deny all"` | | `approvedLabel` | `ReactNode` | `"Approved"` | | `deniedLabel` | `ReactNode` | `"Denied"` | | `errorLabel` | `ReactNode` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `undo` | `boolean \| number \| "manual"` | `true` | | `timeout` | `number` | `3000` | | `duration` | `number` | `1200` | | `armDelay` | `number` | `0` | | `scopes` | `("once" \| "session" \| "always")[]` | — | | `scopeLabels` | `{ group?, once?, session?, always? }` | `{ group: "Remember", once: "Once", session: "This session", always: "Always" }` | | `note` | `boolean` | `false` | | `noteLabel` | `string` | `"Add a note for the agent"` | | `className` | `string` | — | `phrase` applies when the riskiest call is `"critical"`. The outcome shows once every part is answered the same way. When the pending calls change, a gesture in progress starts over. ### Data attributes The root sets `data-slot="tool-approval"` (or `"tool-approval-batch"`) and `data-state`. The scope choice sets `data-slot="tool-approval-scope"`, and the note field sets `data-slot="tool-approval-note"`. | Value | When | | ----------- | ---------------------------- | | `requested` | Waiting for Approve or Deny. | | `approved` | The part was approved. | | `denied` | The part was denied. | ## Accessibility - Approve uses the same announcements as [Confirm Button](https://sureui.com/llms/confirm-button.md) and [Type to Confirm](https://sureui.com/llms/type-to-confirm.md), including the second-click fallback for a hold. - Only one response is ever sent per call. Deny is disabled during a low-risk undo window and while a response is pending, and both buttons come back if the response fails. In a batch, only the calls whose response failed come back. - The scope choice is a radio group named by `scopeLabels.group`, so arrow keys move between options. - The note field is named by `noteLabel`, and it is disabled with the buttons while a response is pending. ### WCAG - [2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable): `undo="manual"` meets it for `"low"`, since Undo has no time limit. - [2.5.2 Pointer Cancellation](https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation): `"high"` meets it, because releasing early or moving off Approve cancels the hold. --- # Type to Confirm An inline form that unlocks its confirm button only after the exact phrase is typed. Source: https://sureui.com/docs/type-to-confirm ```tsx "use client" import { TypeToConfirm } from "@/components/ui/sureui/type-to-confirm" export default function TypeToConfirmDemo() { return ( ) } ``` ## Installation ```bash npx shadcn@latest add @sureui/type-to-confirm ``` ## Usage ```tsx import { TypeToConfirm } from "@/components/ui/sureui/type-to-confirm" ``` ## Examples ### Acknowledgements Each sentence in `acknowledgements` adds a checkbox. The button unlocks once the phrase matches and every box is checked. ```tsx "use client" import { TypeToConfirm } from "@/components/ui/sureui/type-to-confirm" export default function TypeToConfirmAcknowledgements() { return ( ) } ``` ### Several phrases Pass `phrase` an array to ask for more than one field, such as the project name and a fixed sentence. The button unlocks once every field matches. With an array of phrases, `label` can be an array too, one label per field. ```tsx "use client" import { TypeToConfirm } from "@/components/ui/sureui/type-to-confirm" export default function TypeToConfirmSeveralPhrases() { return ( Enter the project name acme-prod , <> To verify, type delete my project , ]} variant="destructive" confirmLabel="Delete project" onConfirm={deleteProject} className="w-full max-w-sm" /> ) } ``` ### In a dialog [Confirm Dialog](https://sureui.com/llms/confirm-dialog.md) puts the same field in a dialog when you pass it `phrase`. `caseSensitive`, `trim`, `acknowledgements` and an array of phrases work the same way there. ```tsx "use client" import { Button } from "@/components/ui/button" import { ConfirmDialog } from "@/components/ui/sureui/confirm-dialog" export default function TypeToConfirmDialog() { return ( ) } ``` ### Choices Each item in `choices` adds a checkbox that doesn't block confirming. `onConfirm` receives their values by `name`, as they were when the person confirmed, and they reset to `defaultChecked` afterwards. ```tsx "use client" import { TypeToConfirm } from "@/components/ui/sureui/type-to-confirm" export default function TypeToConfirmChoices() { return ( ) } ``` ### Consequences Pass a [Consequences](https://sureui.com/llms/consequences.md) list to show what the action removes, above the input. ```tsx "use client" import { Consequences } from "@/components/ui/sureui/consequences" import { TypeToConfirm } from "@/components/ui/sureui/type-to-confirm" export default function TypeToConfirmConsequences() { return ( } variant="destructive" confirmLabel="Delete organization" onConfirm={deleteOrganization} className="w-full max-w-sm" /> ) } ``` ### Case and spaces The phrase must match exactly. `caseSensitive={false}` ignores case, and `trim` ignores spaces at either end. ```tsx "use client" import { TypeToConfirm } from "@/components/ui/sureui/type-to-confirm" export default function TypeToConfirmCaseAndSpaces() { return ( ) } ``` ### Labels and actions `label` replaces the text above the input. `renderActions` wraps the confirm button, for example to add Cancel next to it. ```tsx "use client" import { Button } from "@/components/ui/button" import { TypeToConfirm } from "@/components/ui/sureui/type-to-confirm" export default function TypeToConfirmLabelsAndActions() { return ( Type acme-prod to move it to the Globex team } confirmLabel="Transfer project" onConfirm={transferProject} renderActions={(confirmButton) => (
{confirmButton}
)} className="w-full max-w-sm" /> ) } ``` ## API reference ### TypeToConfirm Renders a `form`. | Prop | Type | Default | | ------------------ | ----------------------------------------------------------------------- | ---------------------------- | | `phrase` | `string \| string[]` | required | | `onConfirm` | `(choices: Record) => void \| Promise` | required | | `onCancel` | `() => void` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `caseSensitive` | `boolean` | `true` | | `trim` | `boolean` | `false` | | `acknowledgements` | `string[]` | `[]` | | `choices` | `{ name: string, label: ReactNode, defaultChecked?: boolean }[]` | `[]` | | `consequences` | `ReactNode` | — | | `label` | `ReactNode \| ReactNode[]` | `"Type {phrase} to confirm"` | | `confirmLabel` | `ReactNode` | `"Confirm"` | | `undoLabel` | `ReactNode` | `"Undo"` | | `errorLabel` | `ReactNode` | — | | `variant` | [`Button`](https://ui.shadcn.com/docs/components/base/button) `variant` | `"default"` | | `undo` | `boolean \| number \| "manual"` | — | | `pauseUndoOnHover` | `boolean` | `true` | | `pauseUndoOnFocus` | `boolean` | `true` | | `renderActions` | `(confirmButton: ReactElement) => ReactNode` | — | | `className` | `string` | — | | `announcements` | `{ match?, undo?, error? }` | — | ## Accessibility - A matching phrase is announced in a polite live region. Change the text with `announcements`. With several phrases, each field has its own label and announces its own match. - While `onConfirm` is pending, the input is read-only and the button keeps focus. - The inputs and checkboxes clear after confirming, so an undone form isn't one click from running again. - `onConfirmError` and `errorLabel` work as on [Confirm Button](https://sureui.com/llms/confirm-button.md). The form still clears, so a retry means typing the phrase again. ### WCAG - [2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable): `undo="manual"` meets it, since Undo has no time limit. --- # Undo Toast A function that shows a toast with Undo and resolves whether the action should run. Source: https://sureui.com/docs/undo-toast ```tsx "use client" import { Button } from "@/components/ui/button" import { undoToast } from "@/components/ui/sureui/undo-toast" export default function UndoToastDemo() { async function archive() { if (await undoToast("Archived 3 messages")) archiveMessages() } return } ``` ## Installation ```bash npx shadcn@latest add @sureui/undo-toast ``` It needs the shadcn [`Toaster`](https://ui.shadcn.com/docs/components/base/sonner) in your root layout. See [Installation](https://sureui.com/llms/installation.md). ## Usage ```tsx import { undoToast } from "@/components/ui/sureui/undo-toast" if (await undoToast("Moved 3 files to the trash")) { await deleteFiles(ids) } ``` It resolves `true` when the toast closes and `false` on Undo. ## Examples ### Description `description` adds a second line under the message. ```tsx "use client" import { Button } from "@/components/ui/button" import { undoToast } from "@/components/ui/sureui/undo-toast" export default function UndoToastDescription() { async function remove() { const confirmed = await undoToast("Deleted feature/billing-v2", { description: "14 commits that aren't on main", }) if (confirmed) deleteBranch("feature/billing-v2") } return } ``` ### Duration The window lasts 5 seconds, or the milliseconds you pass to `duration`, between 4 and 60 seconds. ```tsx "use client" import { Button } from "@/components/ui/button" import { undoToast } from "@/components/ui/sureui/undo-toast" export default function UndoToastDuration() { async function remove() { const confirmed = await undoToast("Removed 4 members", { duration: 10000, }) if (confirmed) removeMembers() } return } ``` With `duration: "manual"`, the toast stays until people press Undo or dismiss it. It shows [Sonner](https://sonner.emilkowal.ski)'s close button, and there is no countdown. ### Pausing The window pauses while the toast is hovered or focused, and while the tab is hidden. This one sets `pauseUndoOnHover` and `pauseUndoOnFocus` to `false`. ```tsx "use client" import { Button } from "@/components/ui/button" import { undoToast } from "@/components/ui/sureui/undo-toast" export default function UndoToastPausing() { async function markRead() { const confirmed = await undoToast("Marked 28 notifications as read", { pauseUndoOnHover: false, pauseUndoOnFocus: false, }) if (confirmed) markAllRead() } return } ``` ## API reference ### undoToast(message, options) Returns a `Promise`. | Prop | Type | Default | | ------------------ | -------------------- | -------- | | `message` | `ReactNode` | required | | `description` | `ReactNode` | — | | `duration` | `number \| "manual"` | `5000` | | `undoLabel` | `ReactNode` | `"Undo"` | | `pauseUndoOnHover` | `boolean` | `true` | | `pauseUndoOnFocus` | `boolean` | `true` | ## Accessibility - Sonner shows toasts in a polite live region, so the message is announced. - Sonner's hotkey, Alt+T by default, moves focus to the toasts, and focus there pauses the window. - With `prefers-reduced-motion`, the countdown on Undo doesn't move and clears when the window ends. ### WCAG - [2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable): `duration: "manual"` meets it, since the toast has no time limit. A numeric `duration` stops at 60 seconds. --- # Undoable A list item or table row that collapses in place to a label and Undo when it's removed. Source: https://sureui.com/docs/undoable ```tsx "use client" import * as React from "react" import { Button } from "@/components/ui/button" import { Undoable } from "@/components/ui/sureui/undoable" export default function UndoableDemo() { const [files, setFiles] = React.useState(["q3-report.pdf", "notes.md"]) return (
    {files.map((file) => ( } label={`Deleted ${file}`} onConfirm={() => { deleteFile(file) setFiles((current) => current.filter((item) => item !== file)) }} > {({ remove }) => ( <> {file} )} ))}
) } ``` ## Installation ```bash npx shadcn@latest add @sureui/undoable ``` ## Usage ```tsx import { Button } from "@/components/ui/button" import { Undoable } from "@/components/ui/sureui/undoable" } label={`Deleted ${file.name}`} onConfirm={() => deleteFile(file.id)} > {({ remove }) => ( <> {file.name} )} ``` `remove` collapses the row to `label` and Undo, and `onConfirm` runs when the window ends. ## Examples ### Table rows Pass `render={}` to wrap a table row. The label and Undo go in one cell that spans every column. ```tsx "use client" import * as React from "react" import { Button } from "@/components/ui/button" import { Undoable } from "@/components/ui/sureui/undoable" import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table" export default function UndoableTable() { const [branches, setBranches] = React.useState([ "feature/billing-v2", "fix/login-redirect", ]) return ( {branches.map((branch) => ( } label={`Deleted ${branch}`} onConfirm={() => { deleteBranch(branch) setBranches((current) => current.filter((item) => item !== branch) ) }} > {({ remove }) => ( <> {branch} )} ))}
) } ``` ### Undo duration The window lasts 5 seconds, or the milliseconds you pass to `undo` between 4 and 60 seconds. ```tsx "use client" import * as React from "react" import { Button } from "@/components/ui/button" import { Undoable } from "@/components/ui/sureui/undoable" export default function UndoableUndoDuration() { const [members, setMembers] = React.useState(["Maya Chen", "Leo Brandt"]) return (
    {members.map((member) => ( } label={`Removed ${member}`} onConfirm={() => { removeMember(member) setMembers((current) => current.filter((item) => item !== member)) }} > {({ remove }) => ( <> {member} )} ))}
) } ``` With `undo="manual"`, Undo stays until people move on: a press outside the row or focus moving elsewhere runs `onConfirm`. ### Focus after removal If focus was in the row when `onConfirm` finishes, it moves to the same control in the next row, else the previous row, else the list, which gets `tabindex="-1"`. Rows still showing Undo are skipped. `focusAfterRemove` receives the row and returns the element to focus instead, or `null` to leave focus alone. Try it with the keyboard in the first example. ### Errors If `onConfirm` throws or rejects, the row comes back. Pass `onConfirmError` to receive the error instead of having it rethrown. ```tsx "use client" import * as React from "react" import { Button } from "@/components/ui/button" import { Undoable } from "@/components/ui/sureui/undoable" export default function UndoableErrors() { const [files, setFiles] = React.useState(["q3-report.pdf", "notes.md"]) return (
    {files.map((file) => ( } label={`Deleted ${file}`} onConfirm={async () => { await deleteFile(file) setFiles((current) => current.filter((item) => item !== file)) }} onConfirmError={showError} > {({ remove }) => ( <> {file} )} ))}
) } ``` ## API reference ### Undoable The table lists the props it adds to the element it renders. | Prop | Type | Default | | ------------------ | ------------------------------------------------- | ------------------------------------- | | `...elementProps` | props of the `render` element | — | | `onConfirm` | `() => void \| Promise` | required | | `onCancel` | `() => void` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `children` | `ReactNode \| (({ remove, state }) => ReactNode)` | — | | `render` | `ReactElement` | `
` | | `label` | `ReactNode` | `"Deleted"` | | `undoLabel` | `ReactNode` | `"Undo"` | | `undo` | `boolean \| number \| "manual"` | `true` | | `pauseUndoOnHover` | `boolean` | `true` | | `pauseUndoOnFocus` | `boolean` | `true` | | `focusAfterRemove` | `(row: HTMLElement) => HTMLElement \| null` | next row, previous row, then the list | | `announcements` | `{ undo? }` | — | ### Data attributes The rendered element sets `data-state`. Drop the item from your data once it's `removed`. After `onConfirm` fails, it also sets `data-error` until the next removal. | Value | When | | --------- | --------------------------------------------------- | | `idle` | Showing its children. | | `undo` | Collapsed, with the undo window open. | | `pending` | `onConfirm` returned a promise that hasn't settled. | | `removed` | `onConfirm` has run. | ## Accessibility - If focus was in the row, it moves to Undo, and back to the button that removed the row after Undo. - Once the row is removed, focus moves to a neighboring row or the list, so it isn't lost to the page. - The label and "Undo is available." are announced in a polite live region. Change the text with `announcements`. - With `prefers-reduced-motion`, the fill on Undo doesn't move. ### WCAG - [2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable): `undo="manual"` meets it, since Undo has no time limit. A numeric `undo` stops at 60 seconds. --- # Unsaved Changes Asks before unsaved changes are lost, when leaving a page or when closing a dialog. Source: https://sureui.com/docs/unsaved-changes ```tsx "use client" import * as React from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { useUnsavedChanges } from "@/components/ui/sureui/unsaved-changes" export default function UnsavedChangesDemo() { const [page, setPage] = React.useState("General") const [name, setName] = React.useState("Acme") const { confirmLeave, dialog } = useUnsavedChanges({ when: name !== "Acme", onDiscard: () => setName("Acme"), }) async function open(to: string) { if (await confirmLeave()) setPage(to) } return (
{page === "General" ? ( setName(event.target.value)} /> ) : (

Billing settings

)} {dialog}
) } ``` ## Installation ```bash npx shadcn@latest add @sureui/unsaved-changes ``` ## Usage ```tsx import { useUnsavedChanges } from "@/components/ui/sureui/unsaved-changes" const { confirmLeave, dialog } = useUnsavedChanges({ when: dirty }) async function openBilling() { if (await confirmLeave()) navigate("/settings/billing") } return ( <>
...
{dialog} ) ``` While `when` is true, reloading or closing the tab shows the browser's own prompt, and `confirmLeave()` opens a dialog with Keep editing and Discard changes. It resolves `true` when it's fine to leave. While `when` is false it resolves `true` right away. The hook doesn't know your router. Call `confirmLeave()` before any navigation you start. ## Examples ### Save from the dialog With `onSave`, the dialog offers Save and stays open until it settles. Discard changes then asks once more before the edits are thrown away. `onDiscard` runs once they are. ```tsx "use client" import * as React from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { useUnsavedChanges } from "@/components/ui/sureui/unsaved-changes" export default function UnsavedChangesSave() { const [saved, setSaved] = React.useState("Ship the beta") const [title, setTitle] = React.useState(saved) const { confirmLeave, dialog } = useUnsavedChanges({ when: title !== saved, onSave: async () => { await saveTask(title) setSaved(title) }, onDiscard: () => setTitle(saved), }) async function close() { if (await confirmLeave()) closeTask() } return (
setTitle(event.target.value)} /> {dialog}
) } ``` ### In a dialog The same hook asks before a Dialog, Sheet or Drawer with unsaved edits closes. It works with one already in your app and doesn't install one. If you don't have one yet, run `npx shadcn@latest add dialog`. ```tsx "use client" import * as React from "react" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogFooter, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { useUnsavedChanges } from "@/components/ui/sureui/unsaved-changes" export default function UnsavedChangesDialog() { const [saved, setSaved] = React.useState("John Doe") const [name, setName] = React.useState(saved) const { rootProps, question, close } = useUnsavedChanges({ when: name !== saved, onDiscard: () => setName(saved), }) function save(event: React.FormEvent) { event.preventDefault() saveProfile(name) setSaved(name) close() } function cancel() { setName(saved) close() } return ( }>Edit profile
Edit profile setName(event.target.value)} /> {question ?? ( <> )}
) } ``` ```tsx const { rootProps, question, close } = useUnsavedChanges({ when: dirty, onDiscard: resetForm, }) {question ?? } ``` Spread `rootProps` on the root. While `when` is `true`, Escape, a click outside or the × button keeps it open and sets `question`: Keep editing and Discard changes, plus Save with `onSave`. Render it in place of your footer buttons, so the dialog keeps its size. While `when` is `false`, it closes straight away and `question` is `null`. `close()` closes without asking. Call it after saving, or from a Cancel button after resetting the form. ## API reference ### useUnsavedChanges(options) | Prop | Type | Default | | ---------------- | -------------------------------- | ------------------------------------ | | `when` | `boolean` | required | | `onSave` | `() => void \| Promise` | — | | `onDiscard` | `() => void \| Promise` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `beforeUnload` | `boolean` | `true` | | `title` | `string` | `"Discard unsaved changes?"` | | `saveTitle` | `string` | `"Save changes before leaving?"` | | `description` | `ReactNode` | `"Your changes haven't been saved."` | | `keepLabel` | `string` | `"Keep editing"` | | `discardLabel` | `string` | `"Discard changes"` | | `saveLabel` | `string` | `"Save"` | | `open` | `boolean` | — | | `onOpenChange` | `(open: boolean) => void` | — | `beforeUnload={false}` leaves reloads and closed tabs alone, for when your router already prompts. `open` and `onOpenChange` control the dialog that `rootProps` goes on. If `onSave` or `onDiscard` returns a promise, its button stays pending until it settles, and nothing closes unless it succeeds. If it fails, the buttons stay and the error goes to `onConfirmError`, as on [Confirm Button](https://sureui.com/llms/confirm-button.md). | Name | Type | Description | | -------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `confirmLeave` | `() => Promise` | Opens the confirm dialog while `when` is true. Resolves `true` once the changes are saved or discarded, and `false` on Keep editing, Escape or unmount. Called again while the dialog is open, it returns the same promise. | | `dialog` | `ReactElement` | The confirm dialog for `confirmLeave`. Render it once. | | `rootProps` | `{ open, onOpenChange }` | Props for your Dialog, Sheet or Drawer root. | | `question` | `ReactElement \| null` | The buttons to render in its footer while asking, otherwise `null`. | | `close` | `() => void` | Closes your dialog without asking. | ## Accessibility - The dialog is a [Confirm Dialog](https://sureui.com/llms/confirm-dialog.md): it has the `alertdialog` role, named by the title and described by the description. - Without `onSave`, Keep editing has focus when the dialog opens, so Enter doesn't throw anything away. - Focus starts on Keep editing, the choice that loses nothing. While Save is pending, the other buttons and Escape do nothing. - Browsers show their own text in the unload prompt, and only after someone has interacted with the page. - In a dialog, the question announces `title`, or `saveTitle` with `onSave`, in a polite live region and moves focus to Keep editing. Keep editing returns focus to where it was, and while it's asking, Escape and outside clicks do nothing more. --- # Confirmation Core useConfirmation, the hook every SureUI control is built on. Put click again, hold, slide and undo on any element. Source: https://sureui.com/docs/confirmation-core ```tsx "use client" import { useConfirmation } from "@/components/ui/sureui/confirmation" export default function ConfirmationCoreHoldCard() { const { state, fillRef, getTriggerProps } = useConfirmation({ gesture: "hold", onConfirm: archiveProject, }) return ( ) } ``` Every SureUI control is built on one hook, `useConfirmation`. It owns the gesture rules, the timing, the undo window and the pending state. The controls only add labels and styling. Use it when you need a confirmation on something SureUI doesn't ship: a card, a toolbar icon, a keyboard shortcut or a canvas tool. `confirmation.ts` comes with every SureUI item, so there's nothing extra to install. ## Usage ```tsx import { useConfirmation } from "@/components/ui/sureui/confirmation" const { state, fillRef, getTriggerProps } = useConfirmation({ gesture: "hold", onConfirm: archiveProject, }) ``` Spread `getTriggerProps` on the element people press. Pass your own handlers to it (`getTriggerProps({ onClick })`) rather than setting them next to it, so yours run first and the hook's still run. Put `fillRef` on an element to get the hold or slide fill; it animates the `scale` property from `scale-x-0`. ## Examples ### Icon button An icon-only button that turns red and asks for a second click. ```tsx "use client" import { Trash2Icon } from "lucide-react" import { Button } from "@/components/ui/button" import { useConfirmation } from "@/components/ui/sureui/confirmation" export default function ConfirmationCoreIconButton() { const { state, getTriggerProps } = useConfirmation({ gesture: "click-again", onConfirm: deleteRow, }) return ( ) } ``` ### Keyboard shortcut A shortcut is a trigger too. Here Backspace clicks the button, so pressing it twice deletes, the way terminals ask you to press Ctrl+C twice. Typing in a field is left alone. ```tsx "use client" import * as React from "react" import { Button } from "@/components/ui/button" import { useConfirmation } from "@/components/ui/sureui/confirmation" export default function ConfirmationCoreShortcut() { const ref = React.useRef(null) const { state, getTriggerProps } = useConfirmation({ gesture: "click-again", onConfirm: deleteIssue, }) React.useEffect(() => { function onKeyDown(event: KeyboardEvent) { const typing = event.target instanceof HTMLElement && event.target.closest("input, textarea, [contenteditable]") if (event.key !== "Backspace" || typing) return event.preventDefault() ref.current?.click() } document.addEventListener("keydown", onKeyDown) return () => document.removeEventListener("keydown", onKeyDown) }, []) return ( ) } ``` ## API reference ### useConfirmation(options) | Prop | Type | Default | | ------------------ | ----------------------------------------------- | --------------- | | `onConfirm` | `() => void \| Promise` | required | | `onCancel` | `() => void` | — | | `onConfirmError` | `(error: unknown) => void` | — | | `gesture` | `"click" \| "click-again" \| "hold" \| "slide"` | `"click"` | | `undo` | `boolean \| number \| "manual"` | `false` | | `pauseUndoOnHover` | `boolean` | `true` | | `pauseUndoOnFocus` | `boolean` | `true` | | `timeout` | `number` | `3000` | | `duration` | `number` | `1200` | | `holdFallback` | `"click-again" \| "none"` | `"click-again"` | | `armDelay` | `number` | `0` | | `wait` | `number` | `0` | | `disabled` | `boolean` | `false` | It returns: - `state`: `"idle" | "armed" | "holding" | "undo" | "pending"`. Set it as `data-state` to style each state. - `failed`: `true` after `onConfirm` failed with `onConfirmError` set, until the next attempt. - `waiting`: the seconds left while `wait` counts down. - `fillRef`: a ref for the fill element. - `getTriggerProps(props)`: your props with the gesture handlers composed in, plus `disabled` while pending or waiting. ## Accessibility - The hook handles pointer, keyboard and focus: Space and Enter hold, a second activation confirms a hold or slide, and leaving the element cancels. - Labels and announcements are yours. Change the visible text or `aria-label` with `state`, as the examples do, and announce armed and undo states in a polite live region. `confirm-button.tsx` shows how the built-in controls do it with `useConfirmationLabels`.