Animated Dialog
by Dax · tsx · 28 lines
A fully accessible modal dialog built on Radix UI primitives with smooth scale + fade animation. Handles focus trapping, Escape to close, and overlay click — all from the headless primitive.
dialogmodalaccessibleradix
28 lines
"use client";
import * as Dialog from "@radix-ui/react-dialog";
import { X } from "lucide-react";
export function Modal({ trigger, title, children }: {
trigger: React.ReactNode;
title: string;
children: React.ReactNode;
}) {
return (
<Dialog.Root>
<Dialog.Trigger asChild>{trigger}</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<Dialog.Content className="fixed left-1/2 top-1/2 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-2xl bg-white p-6 shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=open]:slide-in-from-left-1/2">
<div className="flex items-center justify-between mb-4">
<Dialog.Title className="text-lg font-semibold">{title}</Dialog.Title>
<Dialog.Close className="rounded-full p-1 text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600">
<X size={18} />
</Dialog.Close>
</div>
{children}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}