Modal
A Modal is window containing contextual information, tasks, or workflows that appear over the user interface. The content behind a modal dialog is inert, meaning that users cannot interact with it.
View source codeBasic usage
const Demo = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button text="Open the modal" onClick={() => setIsOpen(true)} />
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Adopt a smooth transition"
iconName="circle-information"
iconVariant="info"
actions={[
<Button
key="cancel"
variant="secondaryNeutral"
text="Cancel"
onClick={() => setIsOpen(false)}
/>,
<Button
key="switch"
variant="primaryBrand"
text="Switch"
onClick={() => setIsOpen(false)}
/>,
]}
>
We recommend closing first before switching.
</Modal>
</>
);
};
Use an illustration
Instead of an icon, use illustration
prop to provide an illustration.
const Demo = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button text="Open the modal" onClick={() => setIsOpen(true)} />
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Adopt a smooth transition"
illustration={<img src="/illustration-2.webp" alt="" />}
actions={[
<Button
key="cancel"
variant="secondaryNeutral"
text="Cancel"
onClick={() => setIsOpen(false)}
/>,
<Button
key="switch"
variant="primaryBrand"
text="Switch"
onClick={() => setIsOpen(false)}
/>,
]}
>
We recommend closing first before switching.
</Modal>
</>
);
};
Lower-level construct
At times, you may need to build complex flows using Modals, or for specific use cases, you might require a highly customized Modal header or Modal footer. In such scenarios, Grapes provides a set of lower-level constructs, allowing you to build any Modal while adhering to the design-system rules.
ModalOverlay
: The dimmed overlay behind the modal dialog.ModalContent
: The container for the modal content. It also contains the button that closes the modal.ModalHeaderWithIcon
: The header that labels the modal dialog with a Grapes icon.ModalHeaderWithIllustration
: The header that labels the modal dialog with an Illustration.ModalBody
: The wrapper that contains the modal's main content.ModalFooter
: The footer that contains the modal actions.
const Demo = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button text="Open the modal" onClick={() => setIsOpen(true)} />
<ModalOverlay isOpen={isOpen}>
<ModalContent
aria-labelledby="grapes-id"
onClose={() => setIsOpen(false)}
>
<ModalHeaderWithIcon
title="Adopt a smooth transition"
iconName="circle-information"
iconVariant="info"
titleId="grapes-id"
/>
<ModalBody>We recommend closing first before switching.</ModalBody>
<ModalFooter>
<Button
key="cancel"
variant="secondaryNeutral"
text="Cancel"
onClick={() => setIsOpen(false)}
/>
<Button
key="switch"
variant="primaryBrand"
text="Switch"
onClick={() => setIsOpen(false)}
/>
</ModalFooter>
</ModalContent>
</ModalOverlay>
</>
);
};
Building a Modal flow
Although it is not the best practice in terms of UX, sometimes you will need to create a flow of modals, i.e. instances where modals follow one another sequentially. To create such flows, you need to use lower-level constructs.
type Action = "next" | "previous" | "open" | "close";
const STATE = {
close: -1,
firstModal: 0,
secondModal: 1,
lastModal: 2,
};
function reducer(state: number, action: Action) {
switch (action) {
case "next":
return state === STATE.lastModal ? STATE.close : state + 1;
case "previous":
return state === STATE.firstModal ? -1 : state - 1;
case "open":
return STATE.firstModal;
case "close":
return STATE.close;
}
}
const Demo = () => {
const [state, setState] = useReducer(reducer, STATE.close);
return (
<>
<Button text="Open the modal" onClick={() => setState("open")} />
<ModalOverlay isOpen={state !== STATE.close}>
<ModalContent
aria-labelledby="grapes-id"
onClose={() => setState("close")}
>
{state === STATE.firstModal && (
<>
<ModalHeaderWithIcon
title="Modal 1"
iconName="pizza"
iconVariant="info"
titleId="grapes-id"
/>
<ModalBody>Content of the first Modal</ModalBody>
</>
)}
{state === STATE.secondModal && (
<>
<ModalHeaderWithIcon
title="Modal 2"
iconName="triangle-warning"
iconVariant="warning"
titleId="grapes-id"
/>
<ModalBody>Content of the second Modal</ModalBody>
</>
)}
{state === STATE.lastModal && (
<>
<ModalHeaderWithIcon
title="Modal 3"
iconName="circle-check"
iconVariant="success"
titleId="grapes-id"
/>
<ModalBody>Content of the third Modal</ModalBody>
</>
)}
<ModalFooter>
<Button
key="cancel"
variant="secondaryNeutral"
text={state === STATE.firstModal ? "Close" : "Previous"}
iconPosition="left"
iconName={state === STATE.firstModal ? undefined : "arrow-left"}
onClick={() => setState("previous")}
/>
<Button
key="switch"
variant="primaryBrand"
text={state === STATE.lastModal ? "Confirm" : "next"}
iconPosition="right"
iconName={state === STATE.lastModal ? undefined : "arrow-right"}
onClick={() => setState("next")}
/>
</ModalFooter>
</ModalContent>
</ModalOverlay>
</>
);
};
Building a Modal without title
In some case, you may want to build a modal without a title, icon nor an illustration. To create such modal, you need to use lower-level constructs.
const Demo = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button text="Open the modal" onClick={() => setIsOpen(true)} />
<ModalOverlay isOpen={isOpen}>
<ModalContent aria-label="Modal without title">
<ModalBody>
<div className="h-[500px] bg-primary-brand-default"></div>
</ModalBody>
<ModalFooter>
<Button onClick={() => setIsOpen(false)} text="Close" />
</ModalFooter>
</ModalContent>
</ModalOverlay>
</>
);
};