Blocks · Overlays
Modal
A confirm dialog behind a trigger button: title, one paragraph, Cancel and the confirm button. Destructive actions get the destructive button.
0.77% of the 2,073 sites in The UI Index have a dialog (16 sites).
Both previews render the same markup; the HTML tab below is that markup without a framework.
Install
- React
- npx shadcn@
latest add https:// www.themes.dev/ ui/ r/ modal.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ ui/ r/ astro/ modal.json
Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the themes.dev UI theme. The HTML needs only the compiled CSS.
/** A confirm modal: a trigger button and a dialog with a title, one paragraph, Cancel and the confirm button. The
* dialog itself (native <dialog>, focus trap, Escape, focus back to the trigger) is the dialog item. When the
* action destroys something the confirm button is the destructive variant, and the trigger matches it. */
export const modal = {
root: 'inline-flex',
};
export const confirmVariant = (destructive?: boolean) => (destructive ? ('destructive' as const) : ('default' as const));
export const triggerVariant = (destructive?: boolean) => (destructive ? ('destructive' as const) : ('outline' as const));import { Button } from '@/components/ui/button';
import { Dialog } from '@/components/ui/dialog';
import { cn } from '@/lib/utils';
import { confirmVariant, modal as c, triggerVariant } from './modal.classes';
export interface ModalProps {
/** Unique on the page: the dialog's id. */
id: string;
/** The button that opens the modal. */
triggerLabel: string;
title: string;
/** One paragraph: what happens when the person confirms. */
description: string;
confirmLabel: string;
cancelLabel?: string;
/** The action deletes or can't be undone: the confirm button (and the trigger) are the destructive variant. */
destructive?: boolean;
className?: string;
}
// command / commandfor: the browser's Invoker Commands (dialog.client.ts fills them in where missing).
export function Modal({ id, triggerLabel, title, description, confirmLabel, cancelLabel = 'Cancel', destructive, className }: ModalProps) {
const opens = { command: 'show-modal', commandfor: id } as Record<string, string>;
return (
<div className={cn(c.root, className)}>
<Button variant={triggerVariant(destructive)} {...opens}>{triggerLabel}</Button>
<Dialog
id={id}
title={title}
description={description}
footer={
<>
<Button variant="ghost" type="submit" value="cancel">{cancelLabel}</Button>
<Button variant={confirmVariant(destructive)} type="submit" value="confirm">{confirmLabel}</Button>
</>
}
/>
</div>
);
}---
import Button from '@/components/ui/button.astro';
import Dialog from '@/components/ui/dialog.astro';
import { cn } from '@/lib/utils';
import { confirmVariant, modal as c, triggerVariant } from './modal.classes';
interface Props {
/** Unique on the page: the dialog's id. */
id: string;
/** The button that opens the modal. */
triggerLabel: string;
title: string;
/** One paragraph: what happens when the person confirms. */
description: string;
confirmLabel: string;
cancelLabel?: string;
/** The action deletes or can't be undone: the confirm button (and the trigger) are the destructive variant. */
destructive?: boolean;
class?: string;
}
const { id, triggerLabel, title, description, confirmLabel, cancelLabel = 'Cancel', destructive, class: className } = Astro.props;
---
<div class={cn(c.root, className)}>
<Button variant={triggerVariant(destructive)} command="show-modal" commandfor={id}>{triggerLabel}</Button>
<Dialog id={id} title={title} description={description}>
<Fragment slot="footer">
<Button variant="ghost" type="submit" value="cancel">{cancelLabel}</Button>
<Button variant={confirmVariant(destructive)} type="submit" value="confirm">{confirmLabel}</Button>
</Fragment>
</Dialog>
</div>/** A confirm modal: a trigger button and a dialog with a title, one paragraph, Cancel and the confirm button. The
* dialog itself (native <dialog>, focus trap, Escape, focus back to the trigger) is the dialog item. When the
* action destroys something the confirm button is the destructive variant, and the trigger matches it. */
export const modal = {
root: 'inline-flex',
};
export const confirmVariant = (destructive?: boolean) => (destructive ? ('destructive' as const) : ('default' as const));
export const triggerVariant = (destructive?: boolean) => (destructive ? ('destructive' as const) : ('outline' as const));<div class="flex flex-wrap items-center justify-center gap-3 p-4">
<div class="inline-flex">
<button type="button" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4" command="show-modal" commandfor="ex-modal-publish">
Publish changes
</button>
<dialog id="ex-modal-publish" data-dialog aria-labelledby="ex-modal-publish-title" aria-describedby="ex-modal-publish-description" class="m-auto w-[calc(100%-2rem)] max-w-md rounded-xl border bg-popover p-0 text-popover-foreground shadow-2xl dark:shadow-[0_24px_64px_rgb(0_0_0/0.7)] backdrop:bg-black/50 backdrop:backdrop-blur-[2px] opacity-0 translate-y-1 transition-[opacity,translate,display,overlay] transition-discrete duration-150 ease-out open:opacity-100 open:translate-y-0 starting:open:opacity-0 starting:open:translate-y-1">
<div class="flex items-start justify-between gap-4 px-5 pt-5">
<div class="min-w-0">
<h2 id="ex-modal-publish-title" class="text-base font-semibold tracking-[-0.01em]">Publish 3 changes?</h2>
<p id="ex-modal-publish-description" class="mt-1 text-sm text-pretty text-muted-foreground">
The changes go live on the site within a minute. You can unpublish them from the dashboard.
</p>
</div>
<form method="dialog" class="-mt-1.5 -mr-2 shrink-0">
<button type="submit" aria-label="Close" class="inline-flex size-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" class="size-4"><path d="M4 4l8 8M12 4l-8 8"></path></svg>
</button>
</form>
</div>
<form method="dialog" data-dialog-footer class="flex flex-wrap items-center justify-end gap-2 border-t bg-muted/50 px-5 py-3">
<button type="submit" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4" value="cancel">
Not yet
</button>
<button type="submit" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] h-9 px-4" value="confirm">
Publish
</button>
</form>
</dialog>
</div>
<div class="inline-flex">
<button type="button" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-destructive text-destructive-foreground hover:bg-[color-mix(in_oklab,var(--destructive)_var(--hover-amount),var(--press-mix-destructive,black))] active:bg-[color-mix(in_oklab,var(--destructive)_var(--press-amount),var(--press-mix-destructive,black))] h-9 px-4" command="show-modal" commandfor="ex-modal-delete">
Delete project
</button>
<dialog id="ex-modal-delete" data-dialog aria-labelledby="ex-modal-delete-title" aria-describedby="ex-modal-delete-description" class="m-auto w-[calc(100%-2rem)] max-w-md rounded-xl border bg-popover p-0 text-popover-foreground shadow-2xl dark:shadow-[0_24px_64px_rgb(0_0_0/0.7)] backdrop:bg-black/50 backdrop:backdrop-blur-[2px] opacity-0 translate-y-1 transition-[opacity,translate,display,overlay] transition-discrete duration-150 ease-out open:opacity-100 open:translate-y-0 starting:open:opacity-0 starting:open:translate-y-1">
<div class="flex items-start justify-between gap-4 px-5 pt-5">
<div class="min-w-0">
<h2 id="ex-modal-delete-title" class="text-base font-semibold tracking-[-0.01em]">Delete Northwind site?</h2>
<p id="ex-modal-delete-description" class="mt-1 text-sm text-pretty text-muted-foreground">
The project, its deploys and its domain settings are removed. This can't be undone.
</p>
</div>
<form method="dialog" class="-mt-1.5 -mr-2 shrink-0">
<button type="submit" aria-label="Close" class="inline-flex size-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" class="size-4"><path d="M4 4l8 8M12 4l-8 8"></path></svg>
</button>
</form>
</div>
<form method="dialog" data-dialog-footer class="flex flex-wrap items-center justify-end gap-2 border-t bg-muted/50 px-5 py-3">
<button type="submit" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4" value="cancel">
Keep it
</button>
<button type="submit" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-destructive text-destructive-foreground hover:bg-[color-mix(in_oklab,var(--destructive)_var(--hover-amount),var(--press-mix-destructive,black))] active:bg-[color-mix(in_oklab,var(--destructive)_var(--press-amount),var(--press-mix-destructive,black))] h-9 px-4" value="confirm">
Delete project
</button>
</form>
</dialog>
</div>
</div>