Grapes homepage1.57.0
  • Guide
  • Tokens
  • Components
    Grapes on GithubGrapes on Figma
    Interaction
    • Button
    • IconButton
    • FloatingActionBar
    • Link
    Icons
    • Icon
    • HighlightIcon
    Form
    • AmountInput
    • Autocomplete
    • AutocompleteMultiple
    • AutocompletePlace
    • CheckboxBox
    • CheckboxField
    • DatePicker
    • FormField
    • Input
    • OptionGroup
    • PasswordInput
    • PhoneInput
    • RadioBox
    • RadioField
    • RadioGroup
    • Select
    • SwitchField
    • TextArea
    • TextInput
    • Upload
    • UploadButton
    Feedback
    • Badge
    • Banner
    • Callout
    • EmptyState
    • Modal
    • ModalSlideshow
    • DeprecatedModalSlideshow
    • PageModal
    • Skeleton
    • Tag
    • Toast
    • Tooltip
    Data display
    • Accordion
    • Avatar
    • Box
    • Calendar
    • CalendarRange
    • CollapsibleList
    • FileCard
    • InfoTip
    • ListBox
    • ListView
    • Panel
    • SidePanel
    • DeprecatedPreview
    • Table
    • Timeline
    • useDateFormatter
    Navigation
    • DropdownItem
    • DropdownMenu
    • Navigation
    • NavigationItem
    • Popover
    • Tabs

    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
    • Usage
    • Props
    • Accessibility

    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.

    Warning

    If you don't use lower-level constructs to build a flow of Modals, animations will be played between each Modal. These animations can be unpleasant for the user. Please use lower-level constructs as demonstrated in the example below.

    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.

    Accessibility concern

    It is recommended to include the aria-label prop in the ModalContent component to ensure the Modal has an accessible title

    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-background-primary-brand-default"></div>
              </ModalBody>
              <ModalFooter>
                <Button onClick={() => setIsOpen(false)} text="Close" />
              </ModalFooter>
            </ModalContent>
          </ModalOverlay>
        </>
      );
    };