Danger Zone
Destructive settings done with care: transfer, archive and delete, each explains its impact; delete needs typing.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/danger-zone.json'use client'
import { useState } from 'react'
import './danger-zone.css'
export interface DangerZoneProps {
name: string
members?: string[]
defaultOpen?: 'transfer' | 'archive' | 'delete' | null
onDelete?: () => void
onArchive?: () => void
onTransfer?: (to: string) => void
fill?: 'dark' | 'light'
className?: string
}
/** Destructive settings with proportionate confirmation. */
export function DangerZone({ name, members = ['leo@fieldnote.co', 'ana@fieldnote.co'], defaultOpen = null, onDelete, onArchive, onTransfer, fill = 'dark', className }: DangerZoneProps) {
const [open, setOpen] = useState(defaultOpen)
const [typed, setTyped] = useState('')
const [to, setTo] = useState(members[0])
const [done, setDone] = useState<string | null>(null)
const row = (k: 'transfer' | 'archive' | 'delete', title: string, sub: string, label: string, body: React.ReactNode) => (
<div className="sd-dgz__r">
<button type="button" className="sd-dgz__t" aria-expanded={open === k} onClick={() => { setOpen(open === k ? null : k); setTyped('') }}><span><b>{title}</b><small>{sub}</small></span><em>{open === k ? 'Cancel' : label}</em></button>
{open === k && <div className="sd-dgz__b">{body}</div>}
</div>
)
if (done) return (
<div className={['sd-dgz', className].filter(Boolean).join(' ')} data-fill={fill}>
<div className="sd-dgz__done" role="status"><b>{done}</b><span>{name}</span><button type="button" onClick={() => { setDone(null); setOpen(null); setTyped('') }}>Back to settings</button></div>
</div>
)
return (
<div className={['sd-dgz', className].filter(Boolean).join(' ')} data-fill={fill}>
<div className="sd-dgz__h">Danger zone<small>{name}</small></div>
{row('transfer', 'Transfer ownership', 'Give this project to another member', 'Transfer', <>
<label>New owner<select value={to} onChange={(e) => setTo(e.target.value)}>{members.map((m) => <option key={m}>{m}</option>)}</select></label>
<ul><li>You become an editor.</li><li>Billing moves to the new owner.</li></ul>
<button type="button" onClick={() => { onTransfer?.(to); setDone(`Transferred to ${to}`) }}>Transfer to {to.split('@')[0]}</button>
</>)}
{row('archive', 'Archive project', 'Read-only; restore any time', 'Archive', <>
<ul><li>Nobody can edit until it is restored.</li><li>Links keep working.</li><li>You can restore it from Settings.</li></ul>
<button type="button" onClick={() => { onArchive?.(); setDone('Project archived') }}>Archive project</button>
</>)}
{row('delete', 'Delete project', 'Permanent. Cannot be undone.', 'Delete', <>
<ul><li>All 1,284 files and their history are removed.</li><li>Shared links stop working immediately.</li><li>This cannot be undone.</li></ul>
<label>Type <code>{name}</code> to confirm<input value={typed} onChange={(e) => setTyped(e.target.value)} autoComplete="off" spellCheck={false} aria-label={`Type ${name} to confirm`} /></label>
<button type="button" disabled={typed !== name} onClick={() => { onDelete?.(); setDone('Project deleted') }}>Delete {name} forever</button>
</>)}
</div>
)
}.sd-dgz { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-dgz[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-dgz :where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-dgz :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-dgz .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-dgz { width: min(100%, 340px); grid-template-columns: minmax(0, 1fr); border-radius: 18px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); overflow: hidden; box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-dgz__h { padding: 12px 14px; border-bottom: 1.5px solid var(--fg); font-size: 13px; font-weight: 700; letter-spacing: 0.02em; display: flex; justify-content: space-between; }
.sd-dgz__h small { font-weight: 400; color: var(--mute); font-size: 12px; }
.sd-dgz__r { border-bottom: 1px solid var(--line); }
.sd-dgz__r:last-child { border-bottom: 0; }
.sd-dgz__t { width: 100%; display: flex; align-items: center; gap: 10px; padding: 12px 14px; text-align: left; }
.sd-dgz__t > span { flex: 1; min-width: 0; }
.sd-dgz__t b { display: block; font-size: 13.5px; }
.sd-dgz__t small { color: var(--mute); font-size: 12px; }
.sd-dgz__t em { font-style: normal; font-size: 12px; font-weight: 600; padding: 6px 10px; border-radius: 8px; box-shadow: inset 0 0 0 1px var(--line); flex: none; }
.sd-dgz__t[aria-expanded='true'] em { background: var(--strong) !important; color: var(--on) !important; box-shadow: none; }
.sd-dgz__b { padding: 0 14px 14px; display: grid; gap: 8px; animation: sd-dgz-in 180ms ease-out; }
@keyframes sd-dgz-in { from { opacity: 0; transform: translateY(-4px); } }
.sd-dgz__b ul { margin: 0; padding-left: 18px; font-size: 12.5px; color: var(--mute); display: grid; gap: 2px; }
.sd-dgz__b label { display: grid; gap: 4px; font-size: 12px; color: var(--mute); }
.sd-dgz__b label code { color: var(--fg); font-weight: 700; }
.sd-dgz__b input, .sd-dgz__b select { height: 36px; padding: 0 10px; border: 0; border-radius: 9px; background: var(--soft); color: var(--fg); font: 13px ui-monospace, monospace; outline: 0; }
.sd-dgz__b input:focus { box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-dgz__b button { height: 40px; border-radius: 11px; font-size: 13px; font-weight: 600;background: var(--strong) !important; color: var(--on) !important; }
.sd-dgz__b button:disabled { opacity: 0.3; }
.sd-dgz__done { padding: 40px 14px; text-align: center; display: grid; gap: 6px; }
.sd-dgz__done b { font-size: 16px; }
.sd-dgz__done span { color: var(--mute); font-size: 13px; }
.sd-dgz__done button { justify-self: center; font-size: 12.5px; font-weight: 600; text-decoration: underline; }
@media (prefers-reduced-motion: reduce) { .sd-dgz *, .sd-dgz *::before, .sd-dgz *::after { animation: none !important; transition: none !important; } }Overview
Each action is a row that expands into what will happen and what cannot be undone. Delete needs the project name typed exactly; the button stays disabled until it matches.
Archive is reversible and says so, so the heavy confirmation is saved for the action that needs it.
When to use
- Project, workspace and account settings.
When not to use
- Undoable actions: use a toast with Undo.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/danger-zone.json$
npx shadcn@latest add https://sundaeui.dev/r/danger-zone.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/danger-zone.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/danger-zone.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy danger-zone.tsx and danger-zone.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { DangerZone } from '@/components/sundae/danger-zone'
<DangerZone />Props
| Prop | Type | Default |
|---|---|---|
name*Resource name to type. | string | — |
onDeleteCalled on confirmed delete. | () => void | — |
onArchiveCalled on archive. | () => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Rows are disclosure buttons; the delete field is labelled with the exact phrase.
- Outcomes are announced.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-dgz; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Danger Zone free?
- Yes. Danger Zone is free for personal and commercial projects, with full source on this page.
- How do I install Danger Zone?
- Run npx shadcn@latest add https://sundaeui.dev/r/danger-zone.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Danger Zone support keyboard and screen readers?
- Rows are disclosure buttons; the delete field is labelled with the exact phrase. Outcomes are announced.
- Does Danger Zone respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All forms →DNS Records
Forms · RefinedNotification Matrix
Forms · Refined- Pro
Shortcut Recorder
Forms · Refined Settings List
Forms · RefinedAsync Switch
Forms · Refined- Pro
Feature Flag
Forms · Refined