react-confirm-alert

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.

brand-color.css
.dialog-brand {
  --rca-button-bg: #4f46e5;
}
brand-color.tsx
'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.

round-corners.css
.dialog-round {
  --rca-radius: 24px;
  --rca-button-radius: 999px;
}
round-corners.tsx
'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

sharp-corners.css
.dialog-sharp {
  --rca-radius: 0;
  --rca-button-radius: 0;
}
sharp-corners.tsx
'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.

compact.css
.dialog-compact {
  --rca-width: 320px;
  --rca-padding: 16px;
  --rca-gap: 4px;
}
compact.tsx
'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:

dark-mode.tsx
'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.

full-width-buttons.css
.button-fill {
  flex-grow: 1;
}
full-width-buttons.tsx
'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.

icon-title.css
.title-icon {
  display: flex;
  align-items: center;
  gap: 8px;
}
.title-icon svg {
  flex: none;
  color: #dc2626;
}
icon-title.tsx
'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.

feedback-form.tsx
'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.

tailwind-only.tsx
'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:

ElementMarkup
BodyclassName="react-confirm-alert-body"
Heading (h1, h2 or h3)data-slot="title"
Label and control wrapperdata-slot="field"
Labeldata-slot="label"
input, textarea or selectdata-slot="input"
Error textdata-slot="error"
Button rowclassName="react-confirm-alert-button-group" and data-slot="actions"
Main buttondata-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

VariableDefault3.0What it changes
--rca-button-bg#18181b, dark #fafafa#333Main button background. Also the checkbox and radio accent.
--rca-button-fg#fff, dark #18181b#eeeMain 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#c62828Danger 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#fffPanel background. Also the background of the other buttons.
--rca-fg#71717a, dark #a1a1aa#666Message 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-ring2px solid in the ring colour2px solid HighlightButton outline on keyboard focus. Takes a full outline value.
--rca-backdroprgba(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-shadow0 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

VariableDefault3.0What it changes
--rca-width420px400pxPanel width. The panel never grows wider than the screen.
--rca-padding24px30pxSpace inside the panel.
--rca-radius10px10pxPanel corners.
--rca-button-radius6px5pxButton corners.
--rca-gap8px10pxSpace between buttons.
--rca-fontinherit, your page fontArial, Helvetica, sans-serifFont family.

Motion and layering

VariableDefault3.0What it changes
--rca-duration200ms0.5sLength of the open animation. Closing always takes 150ms.
--rca-easingcubic-bezier(0.32, 0.72, 0, 1)easeCurve of the open animation.
--rca-delay–0.2sWait before the 3.0 fade-in starts.
--rca-blur–2pxBlur on your app behind a 3.0 dialog.
--rca-z-index9999Stack order of the overlay with modal: false. A modal dialog sits in the top layer, above everything.

On this page