Modal
A modal (aka "dialog") component that prevents interaction with the rest of the application. Component renders its children (ModalTitle, ModalContent, ModalFooter) nodes in front of a backdrop component.
Available from eds-core/1.3.0
Quick Start
- Installation
npm install @adaptavant/eds-core
- Import
import { Modal } from '@adaptavant/eds-core';
Features
The Modal Component includes the following built-in features:
- Focus management: Moves focus inside the modal, auto focus the first focusable element(if any) on mount and keeps focus only within Modal until the modal is closed.
- Child components: Only accepts Modal_Primitives such as
ModalTitle, ModalContent, and ModalFooter
as direct children; passing any other child will result in an error. - Full-screen overlay: Occupies the entire screen and disables scrolling of the page content while open, Creates a backdrop, for disabling interaction below the modal.
- Portal rendering: By default, it renders with a built-in Portal to avoid stacking issues, will be mounted to the
<Root>
component, and accepts z-index and other classNames via style API props. - Edge spacing and Responsive: Ensures modal contents never touch the edge of the screen, maintaining a 16px space. Even though "size" prop is larger than viewport, Modal will never go out of the viewport.
- Accessibility: Manages the appropriate ARIA roles and keyboard controls, including closing the modal on "Esc" press.
Basic Usage
All Modal_Primitives come with default styling for spacing and overflow scroll behaviour; it can be overridden by Style APIs(className, classNames, style, styles) if needed.
const [openModal, setOpenModal] = React.useState(false);
function onModalOpen() {
setOpenModal(true);
console.log('modal opened');
document.querySelector('body').style.overflow = 'hidden';
}
function onModalClose() {
setOpenModal(false);
console.log('modal close');
document.querySelector('body').style.overflow = 'unset';
}
return (
<>
<Button onClick={onModalOpen}>Open Modal</Button>
<Modal
classNames={{
'modalWrapper': 'z-10'
}}
descriptionId="modal-description"
onClose={onModalClose}
onEscPress={() => {
console.log('esc key callbacks triggered');
}}
onOverlayClick={() => {
console.log('overlay click callbacks triggered');
}}
open={openModal}
role="dialog"
size="500"
titleId="modal-title"
>
<ModalHeader closeButtonProps={{label: 'Modal Close Button', onClick: onModalClose}}>
<Heading as="h3" className="text-heading-16 font-stronger" id="modal-title">
Modal with Form fields example
</Heading>
</ModalHeader>
<ModalContent className="gap-2 flex flex-col">
<Text className="text-body-14" id="modal-description">
click outside or esc key you can see your callbacks called before
onClose is called
</Text>
<Field label="First name">
<TextInput defaultValue="Test 123123" />
</Field>
<Field label="Last name">
<TextInput placeholder="(optional)" />
</Field>
<Field label="Email">
<TextInput type="email" autoFocus />
</Field>
<Field label="Phone">
<TextInput type="tel" />
</Field>
<Field label="Address">
<TextInput placeholder="Enter street" />
</Field>
<Field label="Links">
<TextInput placeholder="Add facebook / instagram links" />
</Field>
<Field label="Zip">
<TextInput />
</Field>
</ModalContent>
<ModalFooter>
<Text className="mr-auto text-body-12 text-secondary">
Every element inside modal can be composable
</Text>
<Button onClick={onModalClose} variant="neutralTertiary">Cancel</Button>
<Button onClick={onModalClose}>Save</Button>
</ModalFooter>
</Modal>
</>
);
Alert dialog
The Modal can also be rendered as an alertdialog
. See the example below for rendering a Modal as an alert without a close button, and with closing prevented on Esc
press and overlay click.
const [openModal, setOpenModal] = React.useState(false);
function onModalOpen() {
setOpenModal(true);
console.log('modal opened');
document.querySelector('body').style.overflow = 'hidden';
}
function onModalClose() {
setOpenModal(false);
console.log('modal close');
document.querySelector('body').style.overflow = 'unset';
}
return (
<>
<Button onClick={onModalOpen}>Open Modal</Button>
<Modal
classNames={{
'modalWrapper': 'z-10'
}}
closeOnEsc={false}
closeOnOverlayClick={false}
descriptionId="modal-description"
onClose={onModalClose}
open={openModal}
role="alertdialog"
titleId="modal-title"
>
<ModalHeader>
<Heading as="h3" className="text-heading-16 font-stronger" id="modal-title">
Discard unsaved changes?
</Heading>
</ModalHeader>
<ModalContent className="gap-2 flex flex-col">
<Text className="text-body-14" id="modal-description">
By clicking outside(overlay) or esc key you can't close the Modal,
since no "closeButtonProps" in "ModalHeader" element close button also not rendering.
</Text>
</ModalContent>
<ModalFooter>
<Button onClick={onModalClose} variant="neutralTertiary">Discard and leave</Button>
<Button onClick={onModalClose}>Continue</Button>
</ModalFooter>
</Modal>
</>
);
Responsive layout
Utilise mobileFriendly
prop to dynamically swap Modal
into Sheet
in responsive layout.
const [openModal, setOpenModal] = React.useState(false);
function onModalOpen() {
setOpenModal(true);
console.log('modal opened');
document.querySelector('body').style.overflow = 'hidden';
}
function onModalClose() {
setOpenModal(false);
console.log('modal close');
document.querySelector('body').style.overflow = 'unset';
}
return (
<>
<Button onClick={onModalOpen}>Open Modal</Button>
<Modal
classNames={{
'modalWrapper': 'z-10'
}}
descriptionId="modal-description"
onClose={onModalClose}
open={openModal}
role="alertdialog"
titleId="modal-title"
mobileFriendly // By default it is "true", set it `false` to opt out from rendering `Sheet` in responsive view.
>
<ModalHeader closeButtonProps={{label: 'Modal Close Button', onClick: onModalClose}}>
<Heading as="h3" className="text-heading-16 font-stronger" id="modal-title">
Discard unsaved changes?
</Heading>
</ModalHeader>
<ModalContent className="gap-2 flex flex-col">
<Text className="text-body-14" id="modal-description">
By clicking outside(overlay) or esc key you can't close the Modal,
since no "closeButtonProps" in "ModalHeader" element close button also not rendering.
</Text>
<Text className="text-body-14">
This will switch to Sheet for screen less than 900.
</Text>
</ModalContent>
<ModalFooter>
<Button onClick={onModalClose} variant="neutralTertiary">Discard and leave</Button>
<Button onClick={onModalClose}>Continue</Button>
</ModalFooter>
</Modal>
</>
);
With Dropdown
When using dropdown components (FilterMenuPopover
, DropdownMenuPopover
, or ComboboxPopover
) inside a modal, set shouldUsePortal={false}
to maintain proper keyboard accessibility, as portals render outside the modal’s DOM structure.
const [openModal, setOpenModal] = React.useState(false);
const initialOptions = [
{ id: "1", value: "React" },
{ id: "2", value: "Angular" },
{ id: "3", value: "Vue.js" },
{ id: "4", value: "Svelte" },
{ id: "5", value: "Next.js" },
{ id: "6", value: "Nuxt.js" },
];
const [selectedOption, setSelectedOption] = React.useState(
initialOptions[0]
);
const { filteredOptions, getSearchInputProps } = useFilteredOptions({
initialOptions,
searchFunction: ({ options, searchTerm }) => {
if (searchTerm === "") return options;
return options.filter((option) =>
option.value.toLowerCase().includes(searchTerm.toLowerCase())
);
},
});
const closeModal = () => {
setOpenModal(false);
};
return (
<React.Fragment>
<Button
onClick={() => {
setOpenModal(true);
}}
variant="neutralSecondary"
>
Open Modal
</Button>
<Modal
open={openModal}
onClose={closeModal}
descriptionId="modal-description"
titleId="modal-title"
>
<ModalHeader
closeButtonProps={{
label: "Modal Close Button",
onClick: closeModal,
}}
>
<Heading as="h2" className="text-heading-16" id="modal-title">
With Dropdown
</Heading>
</ModalHeader>
<ModalContent>
<Field label="Frontend Framework" size="large">
<FilterMenu>
<FilterMenuTrigger placeholder="Select Framework">
{selectedOption.value}
</FilterMenuTrigger>
<FilterMenuPopover
shouldUsePortal={false} // Set shouldUsePortal={false} to maintain proper keyboard accessibility, as portals render outside the modal’s DOM structure
>
<FilterMenuSearchField label="Search Items">
<FilterMenuSearchInput
{...getSearchInputProps()}
placeholder="Search..."
/>
</FilterMenuSearchField>
<FilterMenuListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(option) => (
<FilterMenuItem
id={option.id}
isSelected={selectedOption.id === option.id}
onClick={() => {
setSelectedOption(option);
}}
>
{option.value}
</FilterMenuItem>
)}
</FilterMenuListbox>
</FilterMenuPopover>
</FilterMenu>
</Field>
<Text style={{ marginTop: 14 }} class="text-body-14 mb-4">
Note: Set shouldUsePortal={"{false}"} to maintain proper keyboard
accessibility, as portals render outside the modal’s DOM
structure
</Text>
</ModalContent>
</Modal>
</React.Fragment>
);
Style API
Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.
Please refer to the Style API documentation for more insights.
Usage guidelines
Do
- Provide Context: Ensure the user has full context when continuing a flow or task.
- Essential Content: Provide essential content that the user must interact with before continuing.
- Confirmation: Use a modal if an action performed by the user needs confirmation.
- Clarity: Make the relationship between the modal and the previous screen clear.
- Succinct Content: Keep the content of the modal succinct and goal-oriented – Remember, you have limited space overall
Don’t
- Simple Feedback: Don’t use a modal to provide simple feedback after the user performs an action. To confirm or acknowledge an action, use a snackbar.
- Non-Essential Modals: Don’t use a modal if messaging can be conveyed on screen (in the main flow) in a less intrusive way.
- Temporary Messaging: Don’t use a modal for temporary messaging with no actions attached.
Best practices
Do
Give the user context so they’re confident in their action. For example, when the user chooses to switch off an integration, a modal appears to detail the consequences of doing so. The user can then confirm whether or not to continue.
Don’t
A modal isn’t used to communicate simple informational messages with no action required.
Do
If asking the user to confirm an action, pose a question using the modal heading. Be concise and front-load the most important words.
Don’t
Avoid long headings that don’t lead with a verb.
Do
Provide a concise description with essential details, such as the consequences of an action.
Don’t
Avoid long descriptions with unnecessary information.
Do
Relate buttons to the entire modal’s content and present the user with clear options. When the modal heading is a question, the buttons should be answers.
Don’t
Avoid using less descriptive button copy that doesn’t include a verb.
Do
The critical variant is used when a button is attached to an action with severe consequences.
Don’t
Avoid using the critical variant when the consequences of an action aren’t severe.
Do
When a modal action opens another modal, the previous modal closes. There is never more than one modal on screen.
Don’t
Modals don’t overlap.
Do
Advanced modals have a simple structure and functionality.
Don’t
Avoid creating a two-column structure with a navigation linked to multiple subpages. Advanced modals should be one page only.