Styling
Change colours, corners, size and layout. Every example opens a real dialog you can copy.
Most looks take one to three CSS variables. Press a button to open the dialog, then copy the code under its example. The examples go from the smallest change to the largest.
Each example puts its variables on a class that it passes to classNames.dialog, so only that dialog changes. To change every dialog in your app, put the same variables on :root and leave classNames out:
:root {
--rca-button-bg: #4f46e5;
}Examples
Brand colour
--rca-button-bg colours the main button. Its hover colour follows by itself.
.dialog-brand {
--rca-button-bg: #4f46e5;
}
'use client'
import { confirm } from 'react-confirm-alert'
import './brand-color.css'
export function BrandColor() {
return (
<button
onClick={() =>
confirm({
title: 'Publish this post?',
message: 'It goes live for everyone right away.',
confirmLabel: 'Publish',
classNames: { dialog: 'dialog-brand' },
})
}
>
Publish
</button>
)
}
Round corners
--rca-radius rounds the panel and --rca-button-radius rounds the buttons. 999px makes a pill.
.dialog-round {
--rca-radius: 24px;
--rca-button-radius: 999px;
}
'use client'
import { alert } from 'react-confirm-alert'
import './round-corners.css'
export function RoundCorners() {
return (
<button
onClick={() =>
alert({
title: 'Saved',
message: 'Your changes are live.',
classNames: { dialog: 'dialog-round' },
})
}
>
Save
</button>
)
}
Sharp corners
.dialog-sharp {
--rca-radius: 0;
--rca-button-radius: 0;
}
'use client'
import { confirm } from 'react-confirm-alert'
import './sharp-corners.css'
export function SharpCorners() {
return (
<button
onClick={() =>
confirm({
title: 'Archive 12 files?',
message: 'You can restore them from the archive.',
confirmLabel: 'Archive',
classNames: { dialog: 'dialog-sharp' },
})
}
>
Archive
</button>
)
}
Compact
A narrower panel with less space inside and between the buttons. The panel never grows wider than the screen.
.dialog-compact {
--rca-width: 320px;
--rca-padding: 16px;
--rca-gap: 4px;
}
'use client'
import { prompt } from 'react-confirm-alert'
import './compact.css'
export function Compact() {
return (
<button
onClick={() =>
prompt({
title: 'New folder',
label: 'Name',
defaultValue: 'Untitled',
classNames: { dialog: 'dialog-compact' },
})
}
>
New folder
</button>
)
}
Dark mode
Dark colours are off until you turn them on. Put class="dark" or data-theme="dark" on an ancestor of the dialog, usually <html>:
<html class="dark">The library does not follow the OS setting by itself, so it cannot disagree with your app's own theme switch. To follow the OS, set the class from prefers-color-scheme in your theme code. To change the dark colours, set --rca-* variables under your dark selector.
The preview puts the attribute on a div around a <Dialog>, which renders in place, so you can see dark mode on a light page:
'use client'
import { useState } from 'react'
import { Dialog, DialogLayout } from 'react-confirm-alert'
// In your app the attribute (or class="dark") goes on <html>.
export function DarkMode() {
const [open, setOpen] = useState(false)
return (
<div data-theme="dark">
<button onClick={() => setOpen(true)}>Sign out</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogLayout
title="Sign out?"
message="You can sign back in at any time."
actions={[
{ label: 'Cancel', onClick: () => setOpen(false) },
{
label: 'Sign out',
variant: 'primary',
onClick: () => setOpen(false),
},
]}
/>
</Dialog>
</div>
)
}
Full-width buttons
classNames.button adds a class to every button. The default CSS sets no flex-grow on buttons, so one line makes them share the row. On screens 480px wide or narrower, the buttons already stack at full width.
.button-fill {
flex-grow: 1;
}
'use client'
import { confirm } from 'react-confirm-alert'
import './full-width-buttons.css'
export function FullWidthButtons() {
return (
<button
onClick={() =>
confirm({
title: 'Leave without saving?',
message: 'Your edits on this page will be lost.',
confirmLabel: 'Leave',
cancelLabel: 'Stay',
classNames: { button: 'button-fill' },
})
}
>
Leave page
</button>
)
}
Icon before the title
title takes any React node, so an icon goes in with the text.
.title-icon {
display: flex;
align-items: center;
gap: 8px;
}
.title-icon svg {
flex: none;
color: #dc2626;
}
'use client'
import { confirm } from 'react-confirm-alert'
import './icon-title.css'
export const WarningIcon = () => (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 3 2 20h20Z" />
<path d="M12 9v4m0 3v.01" />
</svg>
)
export function IconTitle() {
return (
<button
onClick={() =>
confirm({
title: (
<span className="title-icon">
<WarningIcon /> Delete project?
</span>
),
message: 'This deletes acme-web and all of its deploys.',
tone: 'danger',
confirmLabel: 'Delete',
})
}
>
Delete project
</button>
)
}
A form in your own content
Plain label, select, textarea and checkbox elements get the default look with no class names. Your own content lists the rules. Custom UI has more forms.
'use client'
import { DialogLayout, openDialog } from 'react-confirm-alert'
export function FeedbackForm() {
return (
<button
onClick={() =>
openDialog({
render: ({ close, dismiss }) => (
<form onSubmit={(e) => { e.preventDefault(); close() }}>
<DialogLayout
title="Send feedback"
actions={[
{ label: 'Cancel', onClick: dismiss },
{ label: 'Send', variant: 'primary', type: 'submit' },
]}
>
<label htmlFor="topic">Topic</label>
<select id="topic">
<option>Bug</option>
<option>Idea</option>
<option>Question</option>
</select>
<label htmlFor="details">Details</label>
<textarea id="details" data-autofocus required />
<label>
<input type="checkbox" defaultChecked />{' '}
You can email me about this
</label>
</DialogLayout>
</form>
),
})
}
>
Send feedback
</button>
)
}
Tailwind only
unstyled: true drops the default CSS, and classNames styles each slot. To use the same look for every dialog, pass these options once to configureDialogs. The Tailwind recipe shows how to keep the default CSS and add utilities instead.
'use client'
import { confirm } from 'react-confirm-alert'
export const classNames = {
// open:grid, not grid: a display class would also show the dialog
// while it is closed.
dialog: `fixed inset-0 m-0 size-full max-h-none max-w-none open:grid
place-items-center bg-transparent p-4 backdrop:bg-black/40`,
panel: 'w-full max-w-sm rounded-2xl bg-white p-6 shadow-2xl',
title: 'text-lg font-semibold text-zinc-950',
message: 'mt-1 text-sm text-zinc-500',
actions: 'mt-6 flex justify-end gap-2',
button: `rounded-lg px-4 py-2 text-sm font-medium text-zinc-700
data-[variant=primary]:bg-emerald-600
data-[variant=primary]:text-white`,
}
export function TailwindOnly() {
return (
<button
onClick={() =>
confirm({
title: 'Invite 3 people?',
message: 'They get an email with a link to join.',
confirmLabel: 'Send invites',
unstyled: true,
classNames,
})
}
>
Invite
</button>
)
}
Slots
classNames takes a class per slot: dialog, panel, body, title, message, actions, button, field, label, input, error. Classes add up in this order: the 3.0 class (unless unstyled), configureDialogs, the call, then a 3.0 button className.
Your own content
Inside a dialog with the default look, plain h1 to h3, p, label, input, textarea, select and button are styled with no class names. Those rules sit under :where(), so any class you add wins over them, and they still beat a reset such as Tailwind preflight. DialogLayout adds the body and the button row for you. To mark up the parts by hand, use the same markup as prompt:
| Element | Markup |
|---|---|
| Body | className="react-confirm-alert-body" |
Heading (h1, h2 or h3) | data-slot="title" |
| Label and control wrapper | data-slot="field" |
| Label | data-slot="label" |
input, textarea or select | data-slot="input" |
| Error text | data-slot="error" |
| Button row | className="react-confirm-alert-button-group" and data-slot="actions" |
| Main button | data-variant="primary" or data-variant="danger" |
A data-slot="input" control fills the width of the dialog and has a border. A textarea is at least 80px tall and resizes vertically.
<Dialog
open={open}
onOpenChange={setOpen}
aria-labelledby="note-title"
>
<div className="react-confirm-alert-body">
<h1 id="note-title" data-slot="title">Edit note</h1>
<div data-slot="field">
<label htmlFor="note" data-slot="label">Note</label>
<textarea
id="note"
data-slot="input"
value={note}
onChange={(e) => setNote(e.target.value)}
/>
</div>
<div
className="react-confirm-alert-button-group"
data-slot="actions"
>
<button data-variant="primary" onClick={() => setOpen(false)}>
Done
</button>
</div>
</div>
</Dialog>A data-slot rule is more specific than the plain element rules, so use it when your own CSS would otherwise win.
The plain element rules are built to beat Tailwind v4 preflight. Tailwind v3 preflight and @tailwindcss/forms use attribute selectors such as [type='button'], which win over them: there, a plain <button type="button"> loses its background. Use data-slot and data-variant, DialogLayout, or classes in those setups.
Data attributes
The frame has data-rca, data-state (open or closing), data-kind, data-tone, data-depth, data-topmost and, while a button waits, data-busy. Every slot has data-slot; buttons have data-variant.
Unstyled
unstyled: true drops every react-confirm-alert-* class and inline style. Behavior stays: focus, Escape, outside clicks, waiting for exit animations. With unstyled, lock scroll in your own CSS.
Motion
Closing waits for animations and transitions on the frame, its ::backdrop and the panel, so an exit transition on [data-state='closing'] plays out. prefers-reduced-motion: reduce turns the default fade-in off.
Reference
Every CSS variable the default CSS reads. The Default column is what confirm, alert, prompt, openDialog and <Dialog> use. confirmAlert without skin keeps its 3.0 look and reads the values in the 3.0 column. A dash means the 3.0 look does not read that variable.
Colours
| Variable | Default | 3.0 | What it changes |
|---|---|---|---|
--rca-button-bg | #18181b, dark #fafafa | #333 | Main button background. Also the checkbox and radio accent. |
--rca-button-fg | #fff, dark #18181b | #eee | Main button text. In 3.0, the text of every button. |
--rca-button-bg-hover | --rca-button-bg 15% darker | – | Main button on hover. |
--rca-danger | #dc2626; error text #f87171 in dark | #c62828 | Danger button, error text and the border of an invalid input. |
--rca-danger-hover | --rca-danger 15% darker | – | Danger button on hover. |
--rca-bg | #fff, dark #18181b | #fff | Panel background. Also the background of the other buttons. |
--rca-fg | #71717a, dark #a1a1aa | #666 | Message text. |
--rca-title | #09090b, dark #fafafa | – | Title, labels, input text and the text of the other buttons. |
--rca-border | #e4e4e7, dark #3f3f46 | – | Border of the panel, the inputs and the other buttons. |
--rca-ring | #a1a1aa, dark #71717a | – | Input border and glow on focus. |
--rca-focus-ring | 2px solid in the ring colour | 2px solid Highlight | Button outline on keyboard focus. Takes a full outline value. |
--rca-backdrop | rgba(0, 0, 0, 0.5), dark rgba(0, 0, 0, 0.7) | rgba(255, 255, 255, 0.9) | The layer over your page behind the dialog. |
--rca-shadow | 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1) | 0 20px 75px rgba(0, 0, 0, 0.13) | Panel shadow. |
Size and shape
| Variable | Default | 3.0 | What it changes |
|---|---|---|---|
--rca-width | 420px | 400px | Panel width. The panel never grows wider than the screen. |
--rca-padding | 24px | 30px | Space inside the panel. |
--rca-radius | 10px | 10px | Panel corners. |
--rca-button-radius | 6px | 5px | Button corners. |
--rca-gap | 8px | 10px | Space between buttons. |
--rca-font | inherit, your page font | Arial, Helvetica, sans-serif | Font family. |
Motion and layering
| Variable | Default | 3.0 | What it changes |
|---|---|---|---|
--rca-duration | 200ms | 0.5s | Length of the open animation. Closing always takes 150ms. |
--rca-easing | cubic-bezier(0.32, 0.72, 0, 1) | ease | Curve of the open animation. |
--rca-delay | – | 0.2s | Wait before the 3.0 fade-in starts. |
--rca-blur | – | 2px | Blur on your app behind a 3.0 dialog. |
--rca-z-index | 99 | 99 | Stack order of the overlay with modal: false. A modal dialog sits in the top layer, above everything. |