skills/react-modal/SKILL.md
Create modal dialogs in the frontend using a custom Modal component built on top of Radix UI Dialog. Use this skill whenever the user asks to create, add, or modify a modal, dialog, popup, or confirmation prompt in the React application.
npx skillsauth add landim32/awesome-ai-skills react-modalInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill defines the standard approach for creating modal dialogs in this project. All modals MUST use the custom Modal component located at src/components/ui/Modal.tsx, which wraps Radix UI Dialog primitives.
Before creating any modal, ensure @radix-ui/react-dialog is installed:
npm list @radix-ui/react-dialog || npm install @radix-ui/react-dialog
The base Modal component lives at src/components/ui/Modal.tsx. If it does not exist yet, create it with the following implementation:
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { cn } from "../../lib/utils";
const Modal = DialogPrimitive.Root;
const ModalTrigger = DialogPrimitive.Trigger;
const ModalPortal = DialogPrimitive.Portal;
const ModalClose = DialogPrimitive.Close;
const ModalOverlay = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm",
"data-[state=open]:animate-in data-[state=open]:fade-in-0",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
className
)}
{...props}
/>
));
ModalOverlay.displayName = "ModalOverlay";
const ModalContent = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<ModalPortal>
<ModalOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2",
"bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700",
"p-6 duration-200",
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</ModalPortal>
));
ModalContent.displayName = "ModalContent";
const ModalHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 mb-4", className)} {...props} />
);
ModalHeader.displayName = "ModalHeader";
const ModalFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex justify-end gap-3 mt-6", className)}
{...props}
/>
);
ModalFooter.displayName = "ModalFooter";
const ModalTitle = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-gray-900 dark:text-white", className)}
{...props}
/>
));
ModalTitle.displayName = "ModalTitle";
const ModalDescription = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-gray-600 dark:text-gray-400", className)}
{...props}
/>
));
ModalDescription.displayName = "ModalDescription";
export {
Modal,
ModalPortal,
ModalOverlay,
ModalTrigger,
ModalClose,
ModalContent,
ModalHeader,
ModalFooter,
ModalTitle,
ModalDescription,
};
The ConfirmModal is a specialized, ready-to-use confirmation dialog that lives at src/components/ui/ConfirmModal.tsx. It replaces all uses of window.confirm(). If it does not exist yet, create it with the following implementation:
import * as React from "react";
import {
Modal,
ModalContent,
ModalHeader,
ModalFooter,
ModalTitle,
ModalDescription,
ModalClose,
} from "./Modal";
import { cn } from "../../lib/utils";
type ConfirmVariant = "danger" | "warning" | "default";
interface ConfirmModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void | Promise<void>;
title: string;
description?: string;
confirmLabel?: string;
cancelLabel?: string;
variant?: ConfirmVariant;
loading?: boolean;
}
const variantStyles: Record<ConfirmVariant, string> = {
danger:
"bg-red-600 hover:bg-red-700 text-white focus:ring-red-500",
warning:
"bg-yellow-500 hover:bg-yellow-600 text-white focus:ring-yellow-400",
default:
"bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500",
};
export function ConfirmModal({
open,
onOpenChange,
onConfirm,
title,
description,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
variant = "default",
loading = false,
}: ConfirmModalProps) {
const handleConfirm = async () => {
await onConfirm();
onOpenChange(false);
};
return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent className="max-w-md">
<ModalHeader>
<ModalTitle>{title}</ModalTitle>
{description && (
<ModalDescription>{description}</ModalDescription>
)}
</ModalHeader>
<ModalFooter>
<ModalClose asChild>
<button
type="button"
disabled={loading}
className="px-4 py-2 rounded-lg text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
>
{cancelLabel}
</button>
</ModalClose>
<button
type="button"
onClick={handleConfirm}
disabled={loading}
className={cn(
"px-4 py-2 rounded-lg text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed",
variantStyles[variant]
)}
>
{loading ? "Loading..." : confirmLabel}
</button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
Modal component from src/components/ui/Modal.tsx for any modal, dialog, or popup — unless the user explicitly requests an exception.ConfirmModal from src/components/ui/ConfirmModal.tsx for any confirmation prompt (e.g., "Are you sure you want to delete?"). Never build confirmation dialogs manually from Modal primitives.Modal or ConfirmModal wrappers.window.alert(), window.confirm(), or window.prompt() — use ConfirmModal for confirmations, the Modal component for other dialogs, and toast from sonner for alerts.window.confirm())This is the standard way to ask the user to confirm a destructive or important action:
import * as React from "react";
import { ConfirmModal } from "@/components/ui/ConfirmModal";
import { toast } from "sonner";
function DeleteUserButton({ userId }: { userId: string }) {
const [open, setOpen] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const handleDelete = async () => {
setLoading(true);
try {
await deleteUser(userId);
toast.success("User deleted successfully!");
} catch (error) {
toast.error("Failed to delete user.");
} finally {
setLoading(false);
}
};
return (
<>
<button onClick={() => setOpen(true)}>Delete User</button>
<ConfirmModal
open={open}
onOpenChange={setOpen}
onConfirm={handleDelete}
title="Delete User"
description="Are you sure you want to delete this user? This action cannot be undone."
confirmLabel="Delete"
cancelLabel="Cancel"
variant="danger"
loading={loading}
/>
</>
);
}
ConfirmModal variants:
variant="danger" — Red button. Use for destructive actions (delete, remove, revoke).variant="warning" — Yellow button. Use for risky but reversible actions.variant="default" — Blue button. Use for neutral confirmations.For modals with custom content (forms, detail views, etc.):
import {
Modal,
ModalTrigger,
ModalContent,
ModalHeader,
ModalFooter,
ModalTitle,
ModalDescription,
ModalClose,
} from "@/components/ui/Modal";
function Example() {
return (
<Modal>
<ModalTrigger asChild>
<button>Open Modal</button>
</ModalTrigger>
<ModalContent>
<ModalHeader>
<ModalTitle>Modal Title</ModalTitle>
<ModalDescription>
A brief description of what this modal does.
</ModalDescription>
</ModalHeader>
{/* Modal body content goes here */}
<div>Your content here</div>
<ModalFooter>
<ModalClose asChild>
<button>Cancel</button>
</ModalClose>
<button>Confirm</button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
When you need to control the modal state programmatically (e.g., close after an async action):
function ControlledExample() {
const [open, setOpen] = React.useState(false);
const handleConfirm = async () => {
await someAsyncAction();
setOpen(false);
};
return (
<Modal open={open} onOpenChange={setOpen}>
<ModalTrigger asChild>
<button>Open</button>
</ModalTrigger>
<ModalContent>
<ModalHeader>
<ModalTitle>Confirm Action</ModalTitle>
</ModalHeader>
<ModalFooter>
<ModalClose asChild>
<button>Cancel</button>
</ModalClose>
<button onClick={handleConfirm}>Confirm</button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
| Scenario | Component |
|---|---|
| "Are you sure you want to delete?" | ConfirmModal |
| "Discard unsaved changes?" | ConfirmModal |
| Edit form in a dialog | Modal |
| Detail/preview popup | Modal |
| Alert/notification message | toast from sonner (not a modal) |
src/components/ui/ components (Tailwind CSS, cn() utility, dark mode support via dark: variants).className to any Modal sub-component.max-w-lg. Pass a different className to ModalContent to change it (e.g., className="max-w-2xl" for wider modals).The Radix UI Dialog primitive already provides:
Escape key to closearia- attributes via ModalTitle and ModalDescriptionAlways include a ModalTitle for accessibility. If the title should be visually hidden, use sr-only class.
tools
Guides how to integrate the zTools package for ChatGPT, DALL-E image generation, file upload (S3), slug generation, email sending, and document validation in a .NET 8 project. Use when the user wants to use AI features, upload files, generate slugs, send emails, or understand zTools integration.
documentation
Generates a comprehensive, standardized README.md for any project. Use when the user wants to create or regenerate a README file following the project's documentation standard.
development
Create the complete frontend architecture for a new entity in the React application. Generates TypeScript types, service class, context provider, custom hook, and registers the provider in main.tsx. Use this skill when the user asks to create a new entity, feature module, or domain area in the frontend.
development
Create the complete frontend architecture for a new entity in the React application. Generates TypeScript types, service class, context provider, custom hook, and registers the provider in main.tsx. Use this skill when the user asks to create a new entity, feature module, or domain area in the frontend.