A Radix modal dialog with a blurred overlay, centered card surface that fits 360 px screens, header and footer parts, optional close button and animations.
"use client"
import { Button, buttonVariants } from "@/components/ballmac/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ballmac/dialog"
import { Input } from "@/components/ballmac/input"
import { Label } from "@/components/ballmac/label"
export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger className={buttonVariants({ variant: "outline" })}>Edit profile</DialogTrigger>
<DialogContent className="sm:max-w-md">
<form className="grid gap-5" onSubmit={(event) => event.preventDefault()}>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>Update how your name appears to teammates. Changes save when you click Save.</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="profile-name">Name</Label>
<Input id="profile-name" defaultValue="Alex Morgan" autoComplete="name" />
</div>
<div className="grid gap-2">
<Label htmlFor="profile-username">Username</Label>
<Input id="profile-username" defaultValue="@alexm" autoComplete="username" />
</div>
</div>
<DialogFooter>
<DialogClose type="button" className={buttonVariants({ variant: "outline" })}>
Cancel
</DialogClose>
<Button type="submit">Save changes</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/dialogInstall the dependencies.
$ pnpm add radix-ui lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/dialog.tsx// Ballmac UI: Dialog. https://ui.ballmac.com/components/dialog // Based on shadcn/ui's Dialog (MIT, Copyright (c) 2023 shadcn), restyled with a blurred overlay and tw-animate-css transitions. "use client" import * as React from "react" import { XIcon } from "lucide-react" import { Dialog as DialogPrimitive } from "radix-ui" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" function Dialog(props: React.ComponentProps<typeof DialogPrimitive.Root>) { return <DialogPrimitive.Root data-slot="dialog" {...props} /> } function DialogTrigger(props: React.ComponentProps<typeof DialogPrimitive.Trigger>) { return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> } function DialogPortal(props: React.ComponentProps<typeof DialogPrimitive.Portal>) { return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> } function DialogClose(props: React.ComponentProps<typeof DialogPrimitive.Close>) { return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> } function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { return ( <DialogPrimitive.Overlay data-slot="dialog-overlay" className={cn( "fixed inset-0 z-50 bg-black/50 backdrop-blur-[2px]", "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 motion-reduce:animate-none", className )} {...props} /> ) } type DialogContentProps = React.ComponentProps<typeof DialogPrimitive.Content> & { /** Render the top-right close button. */ showCloseButton?: boolean /** Accessible name of the close button. */ closeLabel?: string } function DialogContent({ className, children, showCloseButton = true, closeLabel, ...props }: DialogContentProps) { const msg = useMessages() closeLabel ??= msg("dialog.closeLabel", "Close") return ( <DialogPortal> <DialogOverlay /> <DialogPrimitive.Content data-slot="dialog-content" className={cn( "fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto rounded-xl border bg-card p-5 text-card-foreground shadow-[0_24px_48px_-12px_rgb(0_0_0/0.25)] outline-none sm:p-6", "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 duration-200 motion-reduce:animate-none", className )} {...props} > {children} {showCloseButton ? ( <DialogPrimitive.Close data-slot="dialog-close" aria-label={closeLabel} className="absolute top-3 end-3 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground outline-none transition-[color,background-color,box-shadow] duration-150 hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none [&_svg]:size-4" > <XIcon aria-hidden="true" /> </DialogPrimitive.Close> ) : null} </DialogPrimitive.Content> </DialogPortal> ) } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-header" className={cn("flex flex-col gap-1.5 text-start [[data-slot=dialog-content]:has(>[data-slot=dialog-close])_&]:pe-8", className)} {...props} /> ) } function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-footer" className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} {...props} /> ) } function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) { return ( <DialogPrimitive.Title data-slot="dialog-title" className={cn("text-lg leading-tight font-semibold tracking-tight", className)} {...props} /> ) } function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) { return ( <DialogPrimitive.Description data-slot="dialog-description" className={cn("text-sm text-muted-foreground", className)} {...props} /> ) } export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, type DialogContentProps, }Update the import paths to match your project setup.
Usage
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "@/components/ballmac/dialog"The full example is in the Code tab above.
Examples
Confirm
import { TriangleAlert } from "lucide-react"
import { buttonVariants } from "@/components/ballmac/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ballmac/dialog"
export default function DialogConfirm() {
return (
<Dialog>
<DialogTrigger className={buttonVariants({ variant: "destructive" })}>Delete project</DialogTrigger>
<DialogContent showCloseButton={false} className="sm:max-w-sm">
<DialogHeader>
<div className="mb-2 flex size-10 items-center justify-center rounded-full bg-destructive/10 text-destructive">
<TriangleAlert className="size-5" aria-hidden="true" />
</div>
<DialogTitle>Delete this project?</DialogTitle>
<DialogDescription>
This permanently deletes the project, its 12 deployments and all environment variables. It can't be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose className={buttonVariants({ variant: "outline" })}>Cancel</DialogClose>
<DialogClose className={buttonVariants({ variant: "destructive" })}>Delete project</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}API reference
| Prop | Type | Default |
|---|---|---|
showCloseButtonRender the top-right close button. | boolean | true |
closeLabelAccessible name of the close button. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Escape | Closes the dialog |
| Tab / Shift+Tab | Cycles focus inside the dialog |
| Enter / Space on trigger | Opens the dialog |
Use with AI
Modal window that traps focus. Compose Dialog > DialogTrigger (style it with buttonVariants) + DialogContent with DialogHeader (DialogTitle, DialogDescription) and DialogFooter; use DialogClose with buttonVariants for cancel buttons. Avoid asChild so the files also work in Base UI projects. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Dialog (@ballmac/dialog) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Short forms that shouldn't leave the page (edit profile, rename)
- Confirming destructive actions
- Focused detail views
Not for
- Non-blocking messages (use a toast)
- Long multi-step flows (use a page)
- Content tied to a control that should stay visible (use a popover)
Registry JSON: https://ui.ballmac.com/r/dialog.json
Credits
Based on shadcn/ui Dialog, adapted by Ballmac. Free to use in personal and commercial projects.
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Input
A text input in three heights that match Button, plus InputGroup and InputGroupAddon for leading or trailing icons, units and domains.
Label
An accessible form label built on Radix Label that dims itself when the paired peer control is disabled and never selects text on double-click.
Textarea
A multi-line text field that can grow with its content between minRows and maxRows, then scroll. Plain resizable mode when autoResize is off.