Modals
The Modals component provides an accessible, animated dialog for displaying important content, confirmations, or flows that require user attention. It includes a dimmed overlay, structured header/body/footer slots, keyboard support, and configurable color schemes.
- Simple (Modal)
- Composable (nested)
- Preview
- Code
import { Modal } from './components/ui/modals';
import { Button } from './components/ui/button';
import { Info } from 'lucide-react';
function Example() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open modal</Button>
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Confirm action"
colorScheme="primary"
size="md"
headerIcon={<Info className="h-5 w-5 text-primary" />}
headerIconClassName="bg-primary/10 border-primary/30"
confirmText="Confirm"
cancelText="Cancel"
>
<p className="mb-2">
Are you sure you want to proceed with this action? This cannot be undone.
</p>
<p className="text-sm text-muted-foreground">
Press <kbd>Esc</kbd> or click outside the modal to close it.
</p>
</Modal>
</>
);
}
- Preview
- Code
import { useState, useCallback } from 'react';
import { AnimatePresence } from 'framer-motion';
import {
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Modal,
} from './components/ui/modals';
import { Button } from './components/ui/button';
import { cn } from './utils/cn';
import { Info } from 'lucide-react';
// Use Modal.colorSchemeConfig for the same styles as the Modal component
type ModalWithColorConfig = typeof Modal & { colorSchemeConfig: Record<string, { backdrop: string; content: string; header: string; body: string; footer: string; closeButton: string; cancelButton: string; confirmButton: string; }> };
const colorScheme = 'primary';
const scheme = (Modal as ModalWithColorConfig).colorSchemeConfig[colorScheme];
function ComposableModalExample() {
const [isOpen, setIsOpen] = useState(false);
const close = useCallback(() => setIsOpen(false), []);
const backdropClassName = cn(scheme.backdrop);
const contentClassName = cn(scheme.content);
const headerClassName = cn(scheme.header);
const bodyClassName = cn('border-l-4', scheme.body);
const footerClassName = cn(scheme.footer);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open modal</Button>
<AnimatePresence>
{isOpen && (
<ModalOverlay closeOnOverlayClick onRequestClose={close} backdropClassName={backdropClassName}>
<ModalContent size="md" className={contentClassName}>
<ModalHeader
title="Confirm action"
icon={<Info className="h-5 w-5 text-primary" />}
iconClassName="bg-primary/10 border-primary/30"
showCloseButton
onClose={close}
className={headerClassName}
closeButtonClassName={scheme.closeButton}
/>
<ModalBody className={bodyClassName}>
<p className="mb-2">Are you sure you want to proceed? This cannot be undone.</p>
<p className="text-sm text-muted-foreground">Press Esc or click outside to close.</p>
</ModalBody>
<ModalFooter
confirmText="Confirm"
cancelText="Cancel"
onConfirm={() => { close(); }}
onCancel={close}
className={footerClassName}
cancelButtonClassName={scheme.cancelButton}
confirmButtonClassName={scheme.confirmButton}
/>
</ModalContent>
</ModalOverlay>
)}
</AnimatePresence>
</>
);
}
Key features:
- Overlay with backdrop blur and click-to-close support
- Structured header, body, and footer sections
- Optional header icon to the left of the title
- Confirm and cancel buttons in the footer
- Keyboard support: Esc closes the modal
- Multiple size options:
sm,md,lg,xl,full - Built-in color schemes:
primary,accent,success,warning,destructive,info - Fine-grained color overrides via
colorOverrides
Installation
- CLI
ignix add component modals
Usage
You can use the Modal in two ways:
- Simple — Use the
<Modal>component with props andchildrenfor the body. - Composable (nested) — Compose
ModalOverlay,ModalContent,ModalHeader,ModalBody, andModalFooteryourself. UseModal.colorSchemeConfigto apply the same color schemes.
The demo above includes both Simple (Modal) and Composable (nested) tabs.
Basic usage (Simple)
import { Modal } from './components/ui/modals';
import { Button } from './components/ui/button';
function BasicModalExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open modal</Button>
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Confirm action"
>
<p>Are you sure you want to proceed?</p>
</Modal>
</>
);
}
Composable (nested) usage
Build the modal from primitives for full control over structure and layout. Use Modal.colorSchemeConfig so styles match the built-in schemes:
import { useState, useCallback } from 'react';
import { AnimatePresence } from 'framer-motion';
import {
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Modal,
} from './components/ui/modals';
import { Button } from './components/ui/button';
import { cn } from './utils/cn';
import { Info } from 'lucide-react';
// Type for Modal's static colorSchemeConfig (for TypeScript)
type ModalWithColorConfig = typeof Modal & {
colorSchemeConfig: Record<
string,
{
backdrop: string;
content: string;
header: string;
body: string;
footer: string;
closeButton: string;
cancelButton: string;
confirmButton: string;
}
>;
};
function ComposableModalExample() {
const [isOpen, setIsOpen] = useState(false);
const close = useCallback(() => setIsOpen(false), []);
const colorScheme = 'primary';
const scheme = (Modal as ModalWithColorConfig).colorSchemeConfig[colorScheme];
const backdropClassName = cn(scheme.backdrop);
const contentClassName = cn(scheme.content);
const headerClassName = cn(scheme.header);
const bodyClassName = cn('border-l-4', scheme.body);
const footerClassName = cn(scheme.footer);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open modal</Button>
<AnimatePresence>
{isOpen && (
<ModalOverlay
closeOnOverlayClick
onRequestClose={close}
backdropClassName={backdropClassName}
>
<ModalContent size="md" className={contentClassName}>
<ModalHeader
title="Confirm action"
icon={<Info className="h-5 w-5 text-primary" />}
iconClassName="bg-primary/10 border-primary/30"
showCloseButton
onClose={close}
className={headerClassName}
closeButtonClassName={scheme.closeButton}
/>
<ModalBody className={bodyClassName}>
<p className="mb-2">Are you sure you want to proceed? This cannot be undone.</p>
<p className="text-sm text-muted-foreground">Press Esc or click outside to close.</p>
</ModalBody>
<ModalFooter
confirmText="Confirm"
cancelText="Cancel"
onConfirm={() => close()}
onCancel={close}
className={footerClassName}
cancelButtonClassName={scheme.cancelButton}
confirmButtonClassName={scheme.confirmButton}
/>
</ModalContent>
</ModalOverlay>
)}
</AnimatePresence>
</>
);
}
When using the composable API, you own visibility (e.g. isOpen and close). For Escape key and body scroll lock, add your own useEffect and key listener if needed.
Subcomponents: ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter. Each accepts the same styling and behavior props as the areas of the main Modal component.
With header icon
import { Info } from 'lucide-react';
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="New update available"
headerIcon={<Info className="h-5 w-5 text-primary" />}
headerIconClassName="bg-primary/10 border-primary/30"
>
<p className="mb-2">
A new version of the application is available. Would you like to update now?
</p>
</Modal>
Color schemes
Use the colorScheme prop to switch between predefined visual styles:
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Success"
colorScheme="success"
>
<p>Your changes have been saved successfully.</p>
</Modal>
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Delete item?"
colorScheme="destructive"
>
<p>This action cannot be undone. Are you sure?</p>
</Modal>
Custom colors with colorOverrides
If you need more control, use the colorOverrides prop to pass custom Tailwind classes for specific areas:
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Custom styled modal"
colorScheme="primary"
colorOverrides={{
header: 'border-b border-violet-500/30 bg-gradient-to-r from-violet-500/20 via-fuchsia-500/10 to-violet-500/20',
content: 'border-violet-500/30 shadow-violet-500/20 before:from-violet-500/10 before:to-fuchsia-500/10',
body: 'border-l-violet-500/30',
footer: 'border-t border-violet-500/20 bg-gradient-to-r from-fuchsia-500/5 via-violet-500/10 to-fuchsia-500/5',
closeButton: 'hover:text-violet-600 hover:bg-violet-500/20 focus:ring-violet-500/50',
cancelButton: 'hover:border-violet-500/40 focus:ring-violet-500/50',
confirmButton:
'bg-gradient-to-r from-violet-600 to-violet-500 text-white hover:from-violet-500 hover:to-violet-600 shadow-violet-500/30 border-violet-500/40 focus:ring-violet-500',
}}
>
<p>Use colorOverrides to fully customize the modal appearance.</p>
</Modal>
Sizes
<Modal size="sm" {...commonProps} />
<Modal size="md" {...commonProps} />
<Modal size="lg" {...commonProps} />
<Modal size="xl" {...commonProps} />
<Modal size="full" {...commonProps} />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | false | Whether the modal is visible. |
onClose | () => void | — | Callback fired when the modal should close (overlay click, Esc, footer buttons, close icon, etc.). |
onConfirm | () => void | undefined | Called when the confirm button is clicked. |
onCancel | () => void | undefined | Called when the cancel button is clicked. |
title | React.ReactNode | undefined | Header title text or node. |
children | React.ReactNode | undefined | Modal body content. |
confirmText | string | "Confirm" | Label for the confirm button. |
cancelText | string | "Cancel" | Label for the cancel button. |
showFooter | boolean | true | Whether to render the footer with action buttons. |
showCloseButton | boolean | true | Whether to show the close (X) button in the header. |
closeOnOverlayClick | boolean | true | Whether clicking on the overlay closes the modal. |
closeOnEscape | boolean | true | Whether pressing the Escape key closes the modal. |
size | 'sm' | 'md' | 'lg' | 'xl' | 'full' | "md" | Size variant of the modal. |
colorScheme | 'primary' | 'accent' | 'success' | 'warning' | 'destructive' | 'info' | "primary" | Built-in color scheme controlling header, borders, and button styles. |
colorOverrides | { overlay?, backdrop?, content?, header?, body?, footer?, closeButton?, cancelButton?, confirmButton? } | undefined | Fine-grained Tailwind class overrides for different visual areas. |
className | string | undefined | Additional classes for the modal content container. |
overlayClassName | string | undefined | Additional classes for the overlay wrapper. |
headerClassName | string | undefined | Additional classes for the header. |
bodyClassName | string | undefined | Additional classes for the body. |
footerClassName | string | undefined | Additional classes for the footer. |
headerIcon | React.ReactNode | undefined | Optional icon rendered to the left of the title in the header. |
headerIconClassName | string | undefined | Additional classes for the header icon wrapper. |