Custom UI
Put your own form, list or warning in a dialog. Plain HTML already looks right.
Write the content as plain HTML. Inside a dialog it gets the default look with no class names.
The playground has each example written four ways: openDialog, <Dialog>, confirm with content, and 3.0 customUI.
A form with validation
openDialog<R>() resolves what your content passes to close(). DialogLayout lays out the title, your fields and the buttons.
'use client'
import { useState } from 'react'
import { DialogLayout, openDialog } from 'react-confirm-alert'
function RenameForm({
name,
close,
}: {
name: string
close: (name: string | null) => void
}) {
const [value, setValue] = useState(name)
const [error, setError] = useState('')
return (
<form
onSubmit={(e) => {
e.preventDefault()
if (value.trim()) close(value.trim())
else setError('Give it a name')
}}
>
<DialogLayout
title="Rename file"
actions={[
{ label: 'Cancel', onClick: () => close(null) },
{ label: 'Rename', variant: 'primary', type: 'submit' },
]}
>
<label htmlFor="rename-name">Name</label>
<input
id="rename-name"
data-autofocus
value={value}
aria-invalid={!!error}
onChange={(e) => {
setValue(e.target.value)
setError('')
}}
/>
{error && <p data-slot="error">{error}</p>}
</DialogLayout>
</form>
)
}
export function RenameOpenDialog() {
const [name, setName] = useState('Q3 report.pdf')
async function rename() {
// close() value, or undefined on Escape and outside clicks.
const next = await openDialog<string | null>({
render: ({ close }) => <RenameForm name={name} close={close} />,
}).result
if (next) setName(next)
}
return (
<div className="row">
<span>{name}</span>
<button onClick={rename}>Rename</button>
</div>
)
}
Pick from a list
Plain radios take the accent colour of the main button.
'use client'
import { useState } from 'react'
import { DialogLayout, openDialog } from 'react-confirm-alert'
const members = ['Ana Silva', 'Ben Ito', 'Chai Wong']
function Members({
value,
onChange,
}: {
value: string
onChange: (name: string) => void
}) {
return members.map((m) => (
<label key={m}>
<input
type="radio"
name="member"
value={m}
checked={m === value}
onChange={() => onChange(m)}
/>{' '}
{m}
</label>
))
}
function Picker({
close,
dismiss,
}: {
close: (name: string) => void
dismiss: () => void
}) {
const [picked, setPicked] = useState(members[0]!)
return (
<DialogLayout
title="Assign to"
message="They get an email with a link to the task."
actions={[
{ label: 'Cancel', onClick: dismiss },
{
label: 'Assign',
variant: 'primary',
onClick: () => close(picked),
},
]}
>
<Members value={picked} onChange={setPicked} />
</DialogLayout>
)
}
export function AssignOpenDialog() {
const [owner, setOwner] = useState('Nobody yet')
async function assign() {
const picked = await openDialog<string>({
render: ({ close, dismiss }) => (
<Picker close={close} dismiss={dismiss} />
),
}).result
if (picked) setOwner(picked)
}
return (
<div className="row">
<span>{owner}</span>
<button onClick={assign}>Assign</button>
</div>
)
}
Type the name to delete
An action whose onClick returns a Promise shows a spinner. The dialog stays open until it settles, and Escape waits too.
'use client'
import { useState } from 'react'
import { DialogLayout, openDialog } from 'react-confirm-alert'
const project = 'acme-web'
const deleteProject = () => new Promise((r) => setTimeout(r, 1200))
function DeleteForm({
close,
dismiss,
}: {
close: (deleted: boolean) => void
dismiss: () => void
}) {
const [typed, setTyped] = useState('')
return (
<DialogLayout
title={`Delete ${project}?`}
message="This deletes the project and its deploys."
actions={[
{ label: 'Cancel', onClick: dismiss },
// A Promise keeps the button busy and the dialog open.
{
label: 'Delete',
variant: 'danger',
disabled: typed !== project,
onClick: () => deleteProject().then(() => close(true)),
},
]}
>
<label htmlFor="delete-name">Type {project} to confirm</label>
<input
id="delete-name"
data-autofocus
autoComplete="off"
value={typed}
onChange={(e) => setTyped(e.target.value)}
/>
</DialogLayout>
)
}
export function DeleteOpenDialog() {
const [status, setStatus] = useState('Active')
async function remove() {
const deleted = await openDialog<boolean>({
render: ({ close, dismiss }) => (
<DeleteForm close={close} dismiss={dismiss} />
),
}).result
if (deleted) setStatus('Deleted')
}
return (
<div className="row">
<span>
{project}: {status}
</span>
<button onClick={remove}>Delete</button>
</div>
)
}
customUI from 3.0
customUI keeps its 3.0 look. Add skin: true and your content gets the new look. configureDialogs({ confirmAlert: { skin: true } }) turns it on for every call.
'use client'
import { useState } from 'react'
import { confirmAlert } from 'react-confirm-alert'
type Invite = { email: string; role: string }
function InviteForm({ done }: { done: (invite?: Invite) => void }) {
const [invite, set] = useState<Invite>({
email: '',
role: 'Viewer',
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
done(invite)
}}
>
<h1>Invite a teammate</h1>
<p>They can open every board in this workspace.</p>
<label htmlFor="invite-email">Email</label>
<input
id="invite-email"
type="email"
required
data-autofocus
value={invite.email}
onChange={(e) => set({ ...invite, email: e.target.value })}
/>
<label htmlFor="invite-role">Role</label>
<select
id="invite-role"
value={invite.role}
onChange={(e) => set({ ...invite, role: e.target.value })}
>
<option>Viewer</option>
<option>Editor</option>
<option>Admin</option>
</select>
<div className="react-confirm-alert-button-group">
<button type="button" onClick={() => done()}>
Cancel
</button>
<button data-variant="primary">Send invite</button>
</div>
</form>
)
}
export function InviteCustomUI() {
const [sent, setSent] = useState('No invites yet')
const invite = () =>
confirmAlert({
skin: true,
customUI: ({ onClose }) => (
<InviteForm
done={(i) => {
if (i) setSent(`${i.email} as ${i.role}`)
onClose()
}}
/>
),
})
return (
<div className="row">
<span>{sent}</span>
<button onClick={invite}>Invite</button>
</div>
)
}
Reference
Which API to use
| Use | When you want |
|---|---|
openDialog<R>({ render }) | A value back from your content. |
<Dialog open onOpenChange> | Content that reads your component's state. |
confirm({ content }) | A confirm with something extra under the message. alert and prompt take content too. |
confirmAlert({ skin, customUI }) | The 3.0 API with the new look. |
Elements with the default look
h1 to h3, p, label, input, textarea, select and button. The rules sit under :where(), so any class you add wins. Tailwind v4 preflight does not remove them.
To style the content yourself, add classes, mark it up with data-slot, or drop the default look with unstyled: true.