Skip to main content
Version: 1.0.3

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.

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

ignix add component modals

Usage

You can use the Modal in two ways:

  1. Simple — Use the <Modal> component with props and children for the body.
  2. Composable (nested) — Compose ModalOverlay, ModalContent, ModalHeader, ModalBody, and ModalFooter yourself. Use Modal.colorSchemeConfig to 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

PropTypeDefaultDescription
isOpenbooleanfalseWhether the modal is visible.
onClose() => voidCallback fired when the modal should close (overlay click, Esc, footer buttons, close icon, etc.).
onConfirm() => voidundefinedCalled when the confirm button is clicked.
onCancel() => voidundefinedCalled when the cancel button is clicked.
titleReact.ReactNodeundefinedHeader title text or node.
childrenReact.ReactNodeundefinedModal body content.
confirmTextstring"Confirm"Label for the confirm button.
cancelTextstring"Cancel"Label for the cancel button.
showFooterbooleantrueWhether to render the footer with action buttons.
showCloseButtonbooleantrueWhether to show the close (X) button in the header.
closeOnOverlayClickbooleantrueWhether clicking on the overlay closes the modal.
closeOnEscapebooleantrueWhether 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? }undefinedFine-grained Tailwind class overrides for different visual areas.
classNamestringundefinedAdditional classes for the modal content container.
overlayClassNamestringundefinedAdditional classes for the overlay wrapper.
headerClassNamestringundefinedAdditional classes for the header.
bodyClassNamestringundefinedAdditional classes for the body.
footerClassNamestringundefinedAdditional classes for the footer.
headerIconReact.ReactNodeundefinedOptional icon rendered to the left of the title in the header.
headerIconClassNamestringundefinedAdditional classes for the header icon wrapper.