- Accordion
- Avatar
- Badge
- Breadcrumb
- Button
- Calendar
- Checkbox
- Combobox
- Container
- CurrencyInput
- DistributionSlider
- Drawer
- Dropdown
- Grid
- Heading
- Image
- Input
- InputGroup
- Label
- Logo
- MapPin
- Modal
- NativeSelect
- NumberInput
- OtpInput
- PhoneInput
- Popover
- Progress
- PropertyCalendar
- RadioGroup
- RadioGroupCards
- ResponsiveModal
- ScrollArea
- SearchBar
- SearchBarFallback
- SearchInput
- Select
- Separator
- Spinner
- Switch
- Tabs
- Text
- Textarea
- Toast
- Toggle
- ToggleGroup
- Tooltip
Modal
Modal dialog for presenting focused content and actions
pnpm add @wandercom/design-system-web
The Modal component provides both a high-level composable API and low-level primitives for advanced use cases. The modal includes a built-in close button in the top-right corner, a backdrop overlay, and fade/zoom animations.
Use the simplified Modal component with slots for most use cases:
import { Modal } from '@wandercom/design-system-web/ui/modal';
import { Button } from '@wandercom/design-system-web/ui/button';
export function Example() {
return (
<Modal
trigger={<Button>Open</Button>}
title="Account Settings"
description="Manage your account preferences"
primaryAction={{ label: "Save", action: () => console.log("saved") }}
secondaryAction={{ label: "Cancel", action: () => console.log("cancelled") }}
>
<div>Content here</div>
</Modal>
);
}For advanced composition, use the individual Modal primitives:
import {
ModalRoot,
ModalTrigger,
ModalClose,
ModalOverlay,
ModalPortal,
ModalContent,
ModalHeader,
ModalFooter,
ModalTitle,
ModalDescription,
} from '@wandercom/design-system-web/ui/modal';
export function Example() {
return (
<ModalRoot>
<ModalTrigger asChild>
<button>Open</button>
</ModalTrigger>
<ModalPortal>
<ModalOverlay />
<ModalContent>
<ModalClose />
<ModalHeader>
<ModalTitle>Account Settings</ModalTitle>
<ModalDescription>Manage your account preferences</ModalDescription>
</ModalHeader>
<div>Content here</div>
<ModalFooter>
<button>Cancel</button>
<button>Save</button>
</ModalFooter>
</ModalContent>
</ModalPortal>
</ModalRoot>
);
}The default size constrains the modal to a max width of 520px. Content, header, and footer are all rendered inline.
<Modal
trigger={<Button>Open</Button>}
title="Account Settings"
description="Manage your account preferences"
primaryAction={{ label: "Save", action: handleSave }}
secondaryAction={{ label: "Cancel", action: handleCancel }}
>
<div>Form fields here</div>
</Modal>The size="wide" variant constrains modal height (430-704px), scrolls header and body content together, and pins the footer with a border separator. The secondary action renders as a ghost button in this variant.
<Modal
size="wide"
trigger={<Button>Open</Button>}
title="Edit Property Details"
description="Update property information"
primaryAction={{ label: "Save changes", action: handleSave }}
secondaryAction={{ label: "Cancel", action: handleCancel }}
>
<div>Scrollable content here</div>
</Modal>Control the modal open state programmatically using the open and onOpenChange props:
import { useState } from 'react';
import { Modal } from '@wandercom/design-system-web/ui/modal';
export function ControlledModal() {
const [open, setOpen] = useState(false);
return (
<Modal
open={open}
onOpenChange={setOpen}
trigger={<button>Open modal</button>}
title="Account Settings"
primaryAction={{ label: "Save", action: () => setOpen(false) }}
secondaryAction={{ label: "Cancel", action: () => setOpen(false) }}
>
<p>Modal content</p>
</Modal>
);
}Use primaryAction and secondaryAction for simple button configurations:
<Modal
trigger={<button>Open</button>}
title="Confirm action"
primaryAction={{ label: "Save", action: () => console.log("saved") }}
secondaryAction={{ label: "Cancel", action: () => console.log("cancelled") }}
>
<p>Are you sure you want to save these changes?</p>
</Modal>Use the variant prop to make the primary action destructive:
<Modal
trigger={<button>Delete</button>}
title="Delete item"
variant="destructive"
primaryAction={{ label: "Delete", action: () => console.log("deleted") }}
secondaryAction={{ label: "Cancel", action: () => console.log("cancelled") }}
>
<p>This action cannot be undone.</p>
</Modal>Associate the primary action with a form using formId. The primary action button becomes a submit button linked to the specified form:
<Modal
trigger={<button>Edit profile</button>}
title="Edit Profile"
formId="profile-form"
primaryAction={{ label: "Save changes", action: () => {} }}
>
<form id="profile-form" onSubmit={handleSubmit}>
<input type="text" name="name" />
</form>
</Modal>Set closable={false} to prevent dismissal via the close button, overlay click, or escape key. Use this for forced flows where the user must complete an action:
<Modal
closable={false}
title="Terms of Service"
description="You must accept the terms to continue."
primaryAction={{ label: "Accept", action: handleAccept }}
>
<div>Terms content here</div>
</Modal>Use the slots prop to customize styles for specific parts of the modal:
<Modal
trigger={<button>Open</button>}
title="Custom styles"
slots={{
content: "max-w-lg",
overlay: "bg-black/40",
title: "text-2xl",
description: "text-muted",
header: "pb-4",
footer: "pt-4",
}}
>
Content here
</Modal>trigger?
title?
description?
footer?
secondaryAction?
variant?
size?
formId?
closable?
children?
open?
onOpenChange?
defaultOpen?
onOpenAutoFocus?
onCloseAutoFocus?
modal?
slots?
className?
className?
onOpenAutoFocus?
onCloseAutoFocus?
className?
className?
className?
className?
className?
className?
- Traps focus within the modal when open
- Closes on escape key press and click outside (unless
closable={false}) - Links title and description via
aria-labelledby/aria-describedby - Returns focus to trigger on close