Dialog
A modal overlay for confirmations, decisions, and short flows.
npx shadcn@latest add @velaui/dialogpnpm dlx velaui add @velaui/dialogyarn dlx shadcn@latest add @velaui/dialogbunx --bun shadcn@latest add @velaui/dialogveni add dialogInstalls the copy of vela-ui that veni-builder serves to Venizia apps; veni-builder serves the Radix UI copy only, so this tab exists for that base alone.
npx shadcn@latest add @velaui/dialogpnpm dlx velaui add @velaui/dialogyarn dlx shadcn@latest add @velaui/dialogbunx --bun shadcn@latest add @velaui/dialogExamples
- Default
- Warning decision
- Photo left
- Photo right small
- Error stacked
- Info large
Usage
import {
Dialog,
DialogContent,
DialogTrigger,
DialogHeader,
DialogIcon,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Check01 } from "@/components/icons/check-01";
export default function Example() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="primary">Delete account</Button>
</DialogTrigger>
<DialogContent size="md">
<DialogHeader>
<DialogIcon variant="success">
<Check01 />
</DialogIcon>
<DialogTitle>Delete your account?</DialogTitle>
<DialogDescription>
This action is permanent and cannot be undone. All your data will be
removed.
</DialogDescription>
</DialogHeader>
<DialogFooter direction="right">
<DialogClose asChild>
<Button variant="secondary">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button variant="primary">Confirm</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}import {
Dialog,
DialogContent,
DialogTrigger,
DialogHeader,
DialogIcon,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Check01 } from "@/components/icons/check-01";
export default function Example() {
return (
<Dialog>
<DialogTrigger render={<Button variant="primary" />}>
Delete account
</DialogTrigger>
<DialogContent size="md">
<DialogHeader>
<DialogIcon variant="success">
<Check01 />
</DialogIcon>
<DialogTitle>Delete your account?</DialogTitle>
<DialogDescription>
This action is permanent and cannot be undone. All your data will be
removed.
</DialogDescription>
</DialogHeader>
<DialogFooter direction="right">
<DialogClose render={<Button variant="secondary" />}>
Cancel
</DialogClose>
<DialogClose render={<Button variant="primary" />}>
Confirm
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}Props
Dialog
Takes every prop of DialogProps.
DialogClose
Takes every prop of DialogCloseProps.
DialogCloseButton
Takes every prop of DialogCloseButtonProps.
DialogContent
| Prop | Type | Default |
|---|---|---|
photo | "none" | "left" | "right" | — |
showCloseButton | boolean | true |
size | "sm" | "md" | "lg" | — |
Takes these and every prop of DialogContentProps.
DialogDescription
Takes every prop of DialogDescriptionProps.
DialogFooter
| Prop | Type | Default |
|---|---|---|
direction | "left" | "right" | "full-width" | "stacked" | — |
Takes these and every prop of DialogFooterProps.
DialogHeader
| Prop | Type | Default |
|---|---|---|
align | "center" | "left" | — |
Takes these and every prop of DialogHeaderProps.
DialogIcon
| Prop | Type | Default |
|---|---|---|
variant | "info" | "success" | "warning" | "error" | — |
Takes these and every prop of DialogIconProps.
DialogOverlay
Takes every prop of DialogOverlayProps.
DialogPanel
| Prop | Type | Default |
|---|---|---|
showCloseButton | boolean | true |
Takes these and every prop of DialogPanelProps.
DialogPhoto
Takes every prop of DialogPhotoProps.
DialogPortal
Takes every prop of DialogPortalProps.
DialogTitle
Takes every prop of DialogTitleProps.
DialogTrigger
Takes every prop of DialogTriggerProps.
Dependencies
- npm
class-variance-authority@0.7.1radix-ui@1.6.0- Installs with it
- 1 icon
- npm
@base-ui/react@1.6.0class-variance-authority@0.7.1- Installs with it
- 1 icon
Source
components/ui/dialog.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Dialog as DialogPrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";
import { Close02 } from "@/components/icons/close-02";
import { cn } from "@/lib/utils";
const dialogContentVariants = cva(
[
"fixed top-1/2 left-1/2 z-50 flex w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2",
"overflow-hidden rounded-xl border border-border-default bg-bg-main text-text-strong shadow-md",
"duration-100 outline-none",
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
"data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
].join(" "),
{
variants: {
size: {
sm: "sm:max-w-[400px]",
md: "sm:max-w-[480px]",
lg: "sm:max-w-[560px]",
},
// The photo panel turns the card into a row; without one the card IS the
// padded column, so it carries the padding itself. No `relative` here —
// it would win the position group over the base `fixed` in tailwind-merge,
// and `fixed` already anchors the absolute close button.
photo: {
none: "flex-col gap-4 p-6",
left: "flex-row",
right: "flex-row-reverse",
},
},
compoundVariants: [
{
photo: ["left", "right"],
size: ["sm", "md", "lg"],
// With a photo the card is one fixed width, whatever the size axis says.
class: "sm:max-w-[700px]",
},
],
defaultVariants: {
size: "md",
photo: "none",
},
},
);
const dialogHeaderVariants = cva(
"flex flex-col gap-1 [&>[data-slot=dialog-icon]]:mb-3",
{
variants: {
align: {
left: "items-start text-left",
center: "items-center text-center",
},
},
defaultVariants: {
align: "left",
},
},
);
const dialogIconVariants = cva(
"flex size-12 shrink-0 items-center justify-center rounded-full text-icon-inverse [&_svg]:size-6",
{
variants: {
variant: {
success: "bg-success-middle",
warning: "bg-warning-middle",
error: "bg-negative-middle",
info: "bg-info-middle",
},
},
defaultVariants: {
variant: "success",
},
},
);
const dialogFooterVariants = cva("flex", {
variants: {
direction: {
right: "flex-row items-center justify-end gap-3",
left: "flex-row items-center justify-start gap-3",
"full-width": "flex-row items-center gap-3 *:flex-1",
stacked: "flex-col-reverse gap-2 *:w-full",
},
},
defaultVariants: {
direction: "right",
},
});
export type DialogProps = React.ComponentProps<typeof DialogPrimitive.Root>;
function Dialog({
...props
}: DialogProps) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
export type DialogTriggerProps = React.ComponentProps<typeof DialogPrimitive.Trigger>;
function DialogTrigger({
...props
}: DialogTriggerProps) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
export type DialogPortalProps = React.ComponentProps<typeof DialogPrimitive.Portal>;
function DialogPortal({
...props
}: DialogPortalProps) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
export type DialogCloseProps = React.ComponentProps<typeof DialogPrimitive.Close>;
function DialogClose({
...props
}: DialogCloseProps) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
export type DialogOverlayProps = React.ComponentProps<typeof DialogPrimitive.Overlay>;
function DialogOverlay({
className,
...props
}: DialogOverlayProps) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className,
)}
{...props}
/>
);
}
export type DialogCloseButtonProps = React.ComponentProps<typeof DialogPrimitive.Close>;
/** The 40×40 icon button pinned to the top-right corner of a padded column. */
function DialogCloseButton({
className,
...props
}: DialogCloseButtonProps) {
return (
<DialogPrimitive.Close
data-slot="dialog-close"
className={cn(
"absolute top-0 right-0 inline-flex size-10 items-center justify-center rounded-lg text-icon-muted transition-colors hover:bg-bg-surface-1 focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow focus-visible:outline-none [&_svg]:size-6",
className,
)}
{...props}
>
<Close02 />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
);
}
export type DialogContentProps = React.ComponentProps<typeof DialogPrimitive.Content> &
VariantProps<typeof dialogContentVariants> & {
showCloseButton?: boolean;
};
function DialogContent({
className,
children,
size,
photo,
showCloseButton = true,
...props
}: DialogContentProps) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(dialogContentVariants({ size, photo }), className)}
{...props}
>
{children}
{/* With a photo the close button belongs to DialogPanel, which is the
column it has to anchor to. */}
{showCloseButton && (!photo || photo === "none") && <DialogCloseButton />}
</DialogPrimitive.Content>
</DialogPortal>
);
}
export type DialogPanelProps = React.ComponentProps<"div"> & { showCloseButton?: boolean };
/** The padded content column beside a DialogPhoto. */
function DialogPanel({
className,
children,
showCloseButton = true,
...props
}: DialogPanelProps) {
return (
<div
data-slot="dialog-panel"
className={cn("relative flex flex-1 flex-col gap-4 p-6", className)}
{...props}
>
{children}
{showCloseButton && <DialogCloseButton />}
</div>
);
}
export type DialogPhotoProps = React.ComponentProps<"div">;
function DialogPhoto({ className, ...props }: DialogPhotoProps) {
return (
<div
data-slot="dialog-photo"
className={cn(
"relative w-[300px] shrink-0 self-stretch overflow-hidden bg-bg-surface-1 *:size-full *:object-cover",
className,
)}
{...props}
/>
);
}
export type DialogHeaderProps = React.ComponentProps<"div"> &
VariantProps<typeof dialogHeaderVariants>;
function DialogHeader({
className,
align,
...props
}: DialogHeaderProps) {
return (
<div
data-slot="dialog-header"
className={cn(dialogHeaderVariants({ align }), className)}
{...props}
/>
);
}
export type DialogIconProps = React.ComponentProps<"div"> & VariantProps<typeof dialogIconVariants>;
function DialogIcon({
className,
variant,
...props
}: DialogIconProps) {
return (
<div
data-slot="dialog-icon"
data-variant={variant ?? "success"}
className={cn(dialogIconVariants({ variant }), className)}
{...props}
/>
);
}
export type DialogFooterProps = React.ComponentProps<"div"> & VariantProps<typeof dialogFooterVariants>;
function DialogFooter({
className,
direction,
...props
}: DialogFooterProps) {
return (
<div
data-slot="dialog-footer"
className={cn(dialogFooterVariants({ direction }), className)}
{...props}
/>
);
}
export type DialogTitleProps = React.ComponentProps<typeof DialogPrimitive.Title>;
function DialogTitle({
className,
...props
}: DialogTitleProps) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"text-xl leading-[1.3] font-semibold text-text-strong",
className,
)}
{...props}
/>
);
}
export type DialogDescriptionProps = React.ComponentProps<typeof DialogPrimitive.Description>;
function DialogDescription({
className,
...props
}: DialogDescriptionProps) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm leading-[1.4] text-text-muted *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-text-strong",
className,
)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogCloseButton,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogIcon,
DialogOverlay,
DialogPanel,
DialogPhoto,
DialogPortal,
DialogTitle,
DialogTrigger,
dialogContentVariants,
dialogFooterVariants,
dialogHeaderVariants,
dialogIconVariants,
};components/ui/dialog.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { Close02 } from "@/components/icons/close-02";
import { cn } from "@/lib/utils";
const dialogContentVariants = cva(
[
"fixed top-1/2 left-1/2 z-50 flex w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2",
"overflow-hidden rounded-xl border border-border-default bg-bg-main text-text-strong shadow-md",
"duration-100 outline-none",
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
"data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
].join(" "),
{
variants: {
size: {
sm: "sm:max-w-[400px]",
md: "sm:max-w-[480px]",
lg: "sm:max-w-[560px]",
},
// The photo panel turns the card into a row; without one the card IS the
// padded column, so it carries the padding itself. No `relative` here —
// it would win the position group over the base `fixed` in tailwind-merge,
// and `fixed` already anchors the absolute close button.
photo: {
none: "flex-col gap-4 p-6",
left: "flex-row",
right: "flex-row-reverse",
},
},
compoundVariants: [
{
photo: ["left", "right"],
size: ["sm", "md", "lg"],
// With a photo the card is one fixed width, whatever the size axis says.
class: "sm:max-w-[700px]",
},
],
defaultVariants: {
size: "md",
photo: "none",
},
},
);
const dialogHeaderVariants = cva(
"flex flex-col gap-1 [&>[data-slot=dialog-icon]]:mb-3",
{
variants: {
align: {
left: "items-start text-left",
center: "items-center text-center",
},
},
defaultVariants: {
align: "left",
},
},
);
const dialogIconVariants = cva(
"flex size-12 shrink-0 items-center justify-center rounded-full text-icon-inverse [&_svg]:size-6",
{
variants: {
variant: {
success: "bg-success-middle",
warning: "bg-warning-middle",
error: "bg-negative-middle",
info: "bg-info-middle",
},
},
defaultVariants: {
variant: "success",
},
},
);
const dialogFooterVariants = cva("flex", {
variants: {
direction: {
right: "flex-row items-center justify-end gap-3",
left: "flex-row items-center justify-start gap-3",
"full-width": "flex-row items-center gap-3 *:flex-1",
stacked: "flex-col-reverse gap-2 *:w-full",
},
},
defaultVariants: {
direction: "right",
},
});
export type DialogProps = DialogPrimitive.Root.Props;
function Dialog({ ...props }: DialogProps) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
export type DialogTriggerProps = DialogPrimitive.Trigger.Props;
function DialogTrigger({ ...props }: DialogTriggerProps) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
export type DialogPortalProps = DialogPrimitive.Portal.Props;
function DialogPortal({ ...props }: DialogPortalProps) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
export type DialogCloseProps = DialogPrimitive.Close.Props;
function DialogClose({ ...props }: DialogCloseProps) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
export type DialogOverlayProps = DialogPrimitive.Backdrop.Props;
function DialogOverlay({
className,
...props
}: DialogOverlayProps) {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className,
)}
{...props}
/>
);
}
export type DialogCloseButtonProps = DialogPrimitive.Close.Props;
/** The 40×40 icon button pinned to the top-right corner of a padded column. */
function DialogCloseButton({
className,
...props
}: DialogCloseButtonProps) {
return (
<DialogPrimitive.Close
data-slot="dialog-close"
className={cn(
"absolute top-0 right-0 inline-flex size-10 items-center justify-center rounded-lg text-icon-muted transition-colors hover:bg-bg-surface-1 focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow focus-visible:outline-none [&_svg]:size-6",
className,
)}
{...props}
>
<Close02 />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
);
}
export type DialogContentProps = DialogPrimitive.Popup.Props &
VariantProps<typeof dialogContentVariants> & {
showCloseButton?: boolean;
};
function DialogContent({
className,
children,
size,
photo,
showCloseButton = true,
...props
}: DialogContentProps) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(dialogContentVariants({ size, photo }), className)}
{...props}
>
{children}
{/* With a photo the close button belongs to DialogPanel, which is the
column it has to anchor to. */}
{showCloseButton && (!photo || photo === "none") && <DialogCloseButton />}
</DialogPrimitive.Popup>
</DialogPortal>
);
}
export type DialogPanelProps = React.ComponentProps<"div"> & { showCloseButton?: boolean };
/** The padded content column beside a DialogPhoto. */
function DialogPanel({
className,
children,
showCloseButton = true,
...props
}: DialogPanelProps) {
return (
<div
data-slot="dialog-panel"
className={cn("relative flex flex-1 flex-col gap-4 p-6", className)}
{...props}
>
{children}
{showCloseButton && <DialogCloseButton />}
</div>
);
}
export type DialogPhotoProps = React.ComponentProps<"div">;
function DialogPhoto({ className, ...props }: DialogPhotoProps) {
return (
<div
data-slot="dialog-photo"
className={cn(
"relative w-[300px] shrink-0 self-stretch overflow-hidden bg-bg-surface-1 *:size-full *:object-cover",
className,
)}
{...props}
/>
);
}
export type DialogHeaderProps = React.ComponentProps<"div"> &
VariantProps<typeof dialogHeaderVariants>;
function DialogHeader({
className,
align,
...props
}: DialogHeaderProps) {
return (
<div
data-slot="dialog-header"
className={cn(dialogHeaderVariants({ align }), className)}
{...props}
/>
);
}
export type DialogIconProps = React.ComponentProps<"div"> & VariantProps<typeof dialogIconVariants>;
function DialogIcon({
className,
variant,
...props
}: DialogIconProps) {
return (
<div
data-slot="dialog-icon"
data-variant={variant ?? "success"}
className={cn(dialogIconVariants({ variant }), className)}
{...props}
/>
);
}
export type DialogFooterProps = React.ComponentProps<"div"> & VariantProps<typeof dialogFooterVariants>;
function DialogFooter({
className,
direction,
...props
}: DialogFooterProps) {
return (
<div
data-slot="dialog-footer"
className={cn(dialogFooterVariants({ direction }), className)}
{...props}
/>
);
}
export type DialogTitleProps = DialogPrimitive.Title.Props;
function DialogTitle({ className, ...props }: DialogTitleProps) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"text-xl leading-[1.3] font-semibold text-text-strong",
className,
)}
{...props}
/>
);
}
export type DialogDescriptionProps = DialogPrimitive.Description.Props;
function DialogDescription({
className,
...props
}: DialogDescriptionProps) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm leading-[1.4] text-text-muted *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-text-strong",
className,
)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogCloseButton,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogIcon,
DialogOverlay,
DialogPanel,
DialogPhoto,
DialogPortal,
DialogTitle,
DialogTrigger,
dialogContentVariants,
dialogFooterVariants,
dialogHeaderVariants,
dialogIconVariants,
};