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 code

Basic 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>
    </>
  );
};