Skip to Content

Popup

Custom popups communicate relevant and timely information to users in response to user action or through system-generated messages.

Interaction Modes

Reviewing

Selecting

Install

yarn add @workday/canvas-kit-react

Component Type

Popup

Platform

Web

Component

Sana Canvas

Delivery Channels

Web

Version

16.1.7

Experience Surfaces

Page Body Inline

Component Type

Popup

Platform

Web

Component

Sana Canvas

Delivery Channels

Web

Version

16.1.7

Experience Surfaces

Page Body Inline

Anatomy

Image of a pop container with annotation markers.
  1. Title (Optional): Titles should display the title of the content or dialog.
  2. Content: Popups contain different types of content. Typical types of content include alerts and dialogs.
  3. Buttons(Optional): When there is a user action, use the action bar. When displaying informational content, use in-line buttons.
  4. Close “X” Icon (Optional): Users are able to intentionally dismiss a popup.

Usage Guidance

Popup components are generally used in place of Non-Modal Dialogs. Because Non-Modal Dialogs only minimally obstruct the page, they are ideal for drawing attention to optional, non-critical information or new features while keeping page content still visible. Popups appear within the context of a page and do not interrupt normal workflow.

When to Use

  • Use Popups when needing to customize a popup element beyond the offerings of other popup components such as a Modal, Tooltip, etc.
  • Do make Popups easily dismissible in context of the trigger element.
  • The popup component is used to display content that doesn’t fit the use cases of more specific notification components such as Tooltips, Modals, Dropdown menus, etc.
  • Popups can be used to display confirmation messages, validate user inputs, or display short informational content in the context of a user action.

When to Use Something Else

  • Do not use Popups to display dense information, such as Tables or Multi-View Containers.
  • Popups are easy to dismiss. Consider using a Modal if you require more user attention or interactive form components in your popup.
  • Consider a Toast if you are communicating status or confirmation of the application process to the user.
  • Consider a Menu if the input is a single selection of options.
  • Use a Tooltip to add context a button, link, to other element.
  • See Error and Alert Notifications  for more information on types of notifications and their use cases.

Examples

The Popup component is a generic Compound Component that is used to build popup UIs that are not already covered by Canvas Kit.

Basic Example

The Popup has no pre-defined behaviors built in, therefore the usePopupModel must always be used to create a new model. This model is then used by all behavior hooks to apply additional popup behaviors to the compound component group. The following example creates a typical popup around a target element and adds useCloseOnOutsideClick, useCloseOnEscape, useInitialFocus, useReturnFocus, and useFocusRedirect behaviors. You can read through the hooks section to learn about all the popup behaviors. For accessibility, these behaviors should be included most of the time.

import {DeleteButton} from '@workday/canvas-kit-react/button';
import {Box} from '@workday/canvas-kit-react/layout';
import {
  Popup,
  useCloseOnEscape,
  useCloseOnOutsideClick,
  useFocusRedirect,
  useInitialFocus,
  usePopupModel,
  useReturnFocus,
} from '@workday/canvas-kit-react/popup';
import {createStyles, px2rem} from '@workday/canvas-kit-styling';

const cardStyles = createStyles({
  width: px2rem(400),
});

const bodyStyles = createStyles({
  marginBlock: '0',
});

export default () => {
  const model = usePopupModel();

  useCloseOnOutsideClick(model);
  useCloseOnEscape(model);
  useInitialFocus(model);
  useReturnFocus(model);
  useFocusRedirect(model);

  const handleDelete = () => {
    console.log('Delete Item');
  };

  return (
    <Popup model={model}>
      <Popup.Target as={DeleteButton}>Delete Item</Popup.Target>
      <Popup.Popper placement="top">
        <Popup.Card cs={cardStyles}>
          <Popup.CloseIcon aria-label="Close" />
          <Popup.Heading>Delete Item</Popup.Heading>
          <Popup.Body>
            <Box as="p" cs={bodyStyles}>
              Are you sure you'd like to delete the item titled 'My Item'?
            </Box>
          </Popup.Body>
          <Popup.ButtonGroup>
            <Popup.CloseButton>Cancel</Popup.CloseButton>
            <Popup.CloseButton as={DeleteButton} onClick={handleDelete}>
              Delete
            </Popup.CloseButton>
          </Popup.ButtonGroup>
        </Popup.Card>
      </Popup.Popper>
    </Popup>
  );
};

Initial Focus

If you want focus to move to a specific element when the popup is opened, set the initialFocusRef of the model. This is useful for popups that don’t have a Close icon button near the top right of the popup. In general, we recommend setting focus to the first interactive component inside the popup that is the least destructive action.

import React from 'react';

import {PrimaryButton} from '@workday/canvas-kit-react/button';
import {useUniqueId} from '@workday/canvas-kit-react/common';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {Flex} from '@workday/canvas-kit-react/layout';
import {
  Popup,
  useCloseOnEscape,
  useCloseOnOutsideClick,
  useFocusRedirect,
  useInitialFocus,
  usePopupModel,
  useReturnFocus,
} from '@workday/canvas-kit-react/popup';
import {Text} from '@workday/canvas-kit-react/text';
import {TextInput} from '@workday/canvas-kit-react/text-input';
import {createStyles, px2rem} from '@workday/canvas-kit-styling';
import {system} from '@workday/canvas-tokens-web';

const cardStyles = createStyles({
  width: px2rem(400),
});

const bodyStyles = createStyles({
  marginBlock: '0',
});

const columnStyles = createStyles({
  gap: system.gap.md,
  alignItems: 'flex-start',
});

const InitialFocusOnButton = () => {
  const messageId = useUniqueId();
  const initialFocusRef = React.useRef(null);
  const model = usePopupModel({
    initialFocusRef,
  });

  useCloseOnOutsideClick(model);
  useCloseOnEscape(model);
  useInitialFocus(model);
  useReturnFocus(model);
  useFocusRedirect(model);

  return (
    <Popup model={model}>
      <Popup.Target>Initial focus: OK button</Popup.Target>
      <Popup.Popper placement={'bottom'}>
        <Popup.Card cs={cardStyles} aria-describedby={messageId}>
          <Popup.Heading>Confirmation</Popup.Heading>
          <Popup.Body>
            <Text cs={bodyStyles} id={messageId}>
              Your message has been sent!
            </Text>
          </Popup.Body>
          <Popup.ButtonGroup>
            <Popup.CloseButton as={PrimaryButton} ref={initialFocusRef}>
              OK
            </Popup.CloseButton>
          </Popup.ButtonGroup>
        </Popup.Card>
      </Popup.Popper>
    </Popup>
  );
};

const InitialFocusOnTextInput = () => {
  const descriptionId = useUniqueId();
  const initialFocusRef = React.useRef<HTMLInputElement>(null);
  const model = usePopupModel({
    initialFocusRef,
  });

  useCloseOnOutsideClick(model);
  useCloseOnEscape(model);
  useInitialFocus(model);
  useReturnFocus(model);
  useFocusRedirect(model);

  return (
    <Popup model={model}>
      <Popup.Target>Initial focus: text input</Popup.Target>
      <Popup.Popper placement={'bottom'}>
        <Popup.Card cs={cardStyles} aria-describedby={descriptionId}>
          <Popup.Heading>Quick reply</Popup.Heading>
          <Popup.Body>
            <FormField>
              <FormField.Label>Message</FormField.Label>
              <FormField.Input as={TextInput} ref={initialFocusRef} />
            </FormField>
          </Popup.Body>
          <Popup.ButtonGroup>
            <Popup.CloseButton>Cancel</Popup.CloseButton>
            <Popup.CloseButton as={PrimaryButton}>Send</Popup.CloseButton>
          </Popup.ButtonGroup>
        </Popup.Card>
      </Popup.Popper>
    </Popup>
  );
};

const InitialFocusOnHeading = () => {
  const headingFocusRef = React.useRef<HTMLHeadingElement>(null);
  const model = usePopupModel({
    initialFocusRef: headingFocusRef,
  });

  useCloseOnOutsideClick(model);
  useCloseOnEscape(model);
  useInitialFocus(model);
  useReturnFocus(model);
  useFocusRedirect(model);

  return (
    <Popup model={model}>
      <Popup.Target>Initial focus: heading</Popup.Target>
      <Popup.Popper placement={'bottom'}>
        <Popup.Card cs={cardStyles}>
          <Popup.Heading ref={headingFocusRef} tabIndex={-1}>
            Important notice
          </Popup.Heading>
          <Popup.Body>
            <Text cs={bodyStyles}>Review the summary below before continuing.</Text>
          </Popup.Body>
          <Popup.ButtonGroup>
            <Popup.CloseButton as={PrimaryButton}>Continue</Popup.CloseButton>
          </Popup.ButtonGroup>
        </Popup.Card>
      </Popup.Popper>
    </Popup>
  );
};

export default () => {
  return (
    <Flex cs={columnStyles}>
      <InitialFocusOnButton />
      <InitialFocusOnTextInput />
      <InitialFocusOnHeading />
    </Flex>
  );
};

Accessibility Note: When initial focus lands on a control below the title (such as the OK button in the example above), assign a unique id to supplementary text and pass aria-describedby on Popup.Card. This augments the included aria-labelledby reference to Popup.Heading so screen readers can announce both the heading and any supplementary text automatically. When initial focus is on the heading itself, add tabIndex={-1} to Popup.Heading so the title can receive programmatic focus. Choose where focus goes based on your product and accessibility requirements.

Focus Redirect

Focus management is important to accessibility of popup contents. The following example shows useFocusRedirect being used to manage focus in and out of a Popup. This is very useful for non-modal popups. Focus redirection tries to treat the Popup as if it were inline to the document. Tabbing out of the Popup will close the Popup and move focus to an adjacent focusable element.

import * as React from 'react';

import {DeleteButton, SecondaryButton} from '@workday/canvas-kit-react/button';
import {useUniqueId} from '@workday/canvas-kit-react/common';
import {Box, Flex} from '@workday/canvas-kit-react/layout';
import {
  Popup,
  useCloseOnEscape,
  useCloseOnOutsideClick,
  useFocusRedirect,
  useInitialFocus,
  usePopupModel,
  useReturnFocus,
} from '@workday/canvas-kit-react/popup';
import {createStyles, px2rem} from '@workday/canvas-kit-styling';
import {system} from '@workday/canvas-tokens-web';

const cardStyles = createStyles({
  width: px2rem(400),
});

const bodyStyles = createStyles({
  marginBlock: '0',
});

const flexStyles = createStyles({
  gap: system.gap.md,
  padding: system.padding.xs,
});

export default () => {
  const model = usePopupModel();

  useCloseOnOutsideClick(model);
  useCloseOnEscape(model);
  useInitialFocus(model);
  useReturnFocus(model);
  useFocusRedirect(model);

  const handleDelete = () => {
    console.log('Delete Item');
  };

  const popupId = useUniqueId();
  const visible = model.state.visibility !== 'hidden';
  React.useLayoutEffect(() => {
    if (visible && model.state.stackRef.current) {
      model.state.stackRef.current.setAttribute('id', popupId);
    }
  }, [model.state.stackRef, visible, popupId]);

  return (
    <Popup model={model}>
      <Flex cs={flexStyles}>
        <Popup.Target as={DeleteButton}>Delete Item</Popup.Target>
        <div aria-owns={popupId} style={{position: 'absolute'}}></div>
        <Popup.Popper>
          <Popup.Card cs={cardStyles}>
            <Popup.CloseIcon aria-label="Close" />
            <Popup.Heading>Delete Item</Popup.Heading>
            <Popup.Body>
              <Box as="p" cs={bodyStyles}>
                Are you sure you'd like to delete the item titled 'My Item'?
              </Box>
            </Popup.Body>
            <Popup.ButtonGroup>
              <Popup.CloseButton>Cancel</Popup.CloseButton>
              <Popup.CloseButton as={DeleteButton} onClick={handleDelete}>
                Delete
              </Popup.CloseButton>
            </Popup.ButtonGroup>
          </Popup.Card>
        </Popup.Popper>
        <SecondaryButton>Next Focusable Button</SecondaryButton>
        <SecondaryButton>Focusable Button After Popup</SecondaryButton>
      </Flex>
    </Popup>
  );
};

Accessibility Note: The useFocusRedirect hook will not have any effect on the reading order of a screen reader. Screen reader users may get confused or disoriented when popups are portalled to the bottom of the document body. In this example, we’re testing the use of aria-owns on a sibling <div> element pointing to the Popup.Card component. This remaps the hierarchy of the accessibility tree (in supported browsers) to address the reading order problem. For more information, see Guides > Accessibility > Inline Popups .

Focus Trapping

Focus trapping is similar to the Focus Redirect example, but will trap focus inside the popup instead of redirecting focus to adjacent focusable elements. This is necessary for modal dialogs where users must focus on the contents of the dialog before proceeding.

import * as React from 'react';

import {DeleteButton, SecondaryButton} from '@workday/canvas-kit-react/button';
import {Box, Flex} from '@workday/canvas-kit-react/layout';
import {
  Popup,
  useCloseOnEscape,
  useCloseOnOutsideClick,
  useFocusTrap,
  useInitialFocus,
  usePopupModel,
  useReturnFocus,
} from '@workday/canvas-kit-react/popup';
import {px2rem} from '@workday/canvas-kit-styling';
import {system} from '@workday/canvas-tokens-web';

export default () => {
  const model = usePopupModel();

  useCloseOnOutsideClick(model);
  useCloseOnEscape(model);
  useInitialFocus(model);
  useReturnFocus(model);
  useFocusTrap(model);

  const handleDelete = () => {
    console.log('Delete Item');
  };

  const popupId = 'popup-test-id';
  const visible = model.state.visibility !== 'hidden';
  React.useLayoutEffect(() => {
    if (visible && model.state.stackRef.current) {
      model.state.stackRef.current.setAttribute('id', popupId);
    }
  }, [model.state.stackRef, visible]);

  return (
    <Popup model={model}>
      <Flex cs={{gap: system.gap.sm}}>
        <Popup.Target as={DeleteButton}>Delete Item</Popup.Target>
        <div aria-owns={popupId} style={{position: 'absolute'}} />
        <Popup.Popper>
          <Popup.Card cs={{width: px2rem(400)}}>
            <Popup.CloseIcon aria-label="Close" />
            <Popup.Heading>Delete Item</Popup.Heading>
            <Popup.Body>
              <Box as="p" cs={{marginBlock: '0'}}>
                Are you sure you'd like to delete the item titled 'My Item'?
              </Box>
            </Popup.Body>
            <Popup.ButtonGroup>
              <Popup.CloseButton>Cancel</Popup.CloseButton>
              <Popup.CloseButton as={DeleteButton} onClick={handleDelete}>
                Delete
              </Popup.CloseButton>
            </Popup.ButtonGroup>
          </Popup.Card>
        </Popup.Popper>
        <SecondaryButton>Next Focusable Button</SecondaryButton>
        <SecondaryButton>Focusable Button After Popup</SecondaryButton>
      </Flex>
    </Popup>
  );
};

Accessibility Note: Focus trapping will not prevent mouse users from breaking out of a focus trap, nor will it prevent screen reader users from using virtual reading cursors from breaking out. Consider using Modal instead when you need to focus users’ attention on a specific task inside of a popup..

Multiple Popups

You can render more than one Popup in the same view by giving each its own model. This example pairs Popup with useDialogModel and useModalModel so you can compare focus redirection (Tab / Shift + Tab can move focus out of the first popup) and focus trapping (focus stays inside the second popup until it closes). Opening one does not close the other.

import {useDialogModel} from '@workday/canvas-kit-react/dialog';
import {Flex} from '@workday/canvas-kit-react/layout';
import {useModalModel} from '@workday/canvas-kit-react/modal';
import {Popup} from '@workday/canvas-kit-react/popup';
import {createStyles, px2rem} from '@workday/canvas-kit-styling';
import {system} from '@workday/canvas-tokens-web';

const flexStyles = createStyles({
  gap: system.gap.md,
});

const popupStyles = createStyles({
  width: px2rem(400),
});

export default () => {
  const dialogModel = useDialogModel();
  const modalModel = useModalModel();

  return (
    <Flex cs={flexStyles}>
      <Popup model={dialogModel}>
        <Popup.Target>Focus Redirect Popup</Popup.Target>
        <Popup.Popper>
          <Popup.Card cs={popupStyles}>
            <Popup.CloseIcon aria-label="Close" size="small" />
            <Popup.Heading>Focus Redirect Popup</Popup.Heading>
            <Popup.Body>
              <p>
                This popup uses the dialog model and will allow keyboard focus to escape when users
                press Tab or Shift + Tab.
              </p>
            </Popup.Body>
          </Popup.Card>
        </Popup.Popper>
      </Popup>
      <Popup model={modalModel}>
        <Popup.Target>Focus Trap Popup</Popup.Target>
        <Popup.Popper>
          <Popup.Card cs={popupStyles}>
            <Popup.CloseIcon aria-label="Close" size="small" />
            <Popup.Heading>Focus Trap Popup</Popup.Heading>
            <Popup.Body>
              <p>
                This popup uses the modal model and will trap keyboard focus when users press Tab or
                Shift + Tab.
              </p>
            </Popup.Body>
            <Popup.ButtonGroup>
              <Popup.CloseButton>OK</Popup.CloseButton>
            </Popup.ButtonGroup>
          </Popup.Card>
        </Popup.Popper>
      </Popup>
    </Flex>
  );
};

Nested Popups

If you need nested Popups within the same component, you can create multiple models and pass a unique model to each Popup. Popup comes with a Popup.CloseButton that uses a Button and adds props via the usePopupCloseButton hook to ensure the popups hides and focus is returned. The as can be used in a powerful way to do this by using <Popup.CloseButton as={Popup.CloseButton}> which will mix in click handlers from both popups. This is not very intuitive, however. You can create props that merge a click handler for both Popups by using usePopupCloseButton directly. The second parameter is props to be merged which will effectively hide both popups. Focus management is preserved.

import {SecondaryButton} from '@workday/canvas-kit-react/button';
import {Flex} from '@workday/canvas-kit-react/layout';
import {
  Popup,
  useCloseOnEscape,
  useCloseOnOutsideClick,
  useInitialFocus,
  usePopupCloseButton,
  usePopupModel,
  useReturnFocus,
} from '@workday/canvas-kit-react/popup';
import {system} from '@workday/canvas-tokens-web';

export default () => {
  const popup1 = usePopupModel();
  const popup2 = usePopupModel();

  useCloseOnOutsideClick(popup1);
  useCloseOnEscape(popup1);
  useInitialFocus(popup1);
  useReturnFocus(popup1);

  useCloseOnOutsideClick(popup2);
  useCloseOnEscape(popup2);
  useInitialFocus(popup2);
  useReturnFocus(popup2);

  const closeBothProps = usePopupCloseButton(popup1, usePopupCloseButton(popup2));

  return (
    <>
      <Popup model={popup1}>
        <Popup.Target>Open Popup 1</Popup.Target>
        <Popup.Popper>
          <Popup.Card aria-label="Popup 1">
            <Popup.CloseIcon aria-label="Close" size="small" />
            <Popup.Body>
              <p style={{marginBlockStart: 0, marginBlockEnd: 0}}>Contents of Popup 1</p>
            </Popup.Body>
            <Flex cs={{gap: system.gap.md, padding: system.padding.xs}}>
              <Popup model={popup2}>
                <Popup.Target>Open Popup 2</Popup.Target>
                <Popup.Popper>
                  <Popup.Card aria-label="Popup 2">
                    <Popup.CloseIcon aria-label="Close" size="small" />
                    <Popup.Body>
                      <p style={{marginBlockStart: 0, marginBlockEnd: 0}}>Contents of Popup 2</p>
                    </Popup.Body>
                    <Popup.ButtonGroup>
                      <Popup.CloseButton as={Popup.CloseButton} model={popup1}>
                        Close Both (as)
                      </Popup.CloseButton>
                      <SecondaryButton {...closeBothProps}>Close Both (props)</SecondaryButton>
                    </Popup.ButtonGroup>
                  </Popup.Card>
                </Popup.Popper>
              </Popup>
            </Flex>
          </Popup.Card>
        </Popup.Popper>
      </Popup>
    </>
  );
};

Accessibility Note: In this example, observe how users can traverse both opened popups using the keyboard. This is likely to be a confusing experience for users and may necessitate focus trapping inside each popup with careful consideration for setting initial focus and returning focus.

Custom Target

It is common to have a custom target for your popup. Use the as prop to use your custom component. The Popup.Target element will add onClick and ref to the provided component. Your provided target component must forward the onClick to an element for the Popup to open. The as will cause Popup.Target to inherit the interface of your custom target component. This means any props your target requires, Popup.Target now also requires. The example below has a MyTarget component that requires a label prop.

Note: If your application needs to programmatically open a Popup without the user interacting with the target button first, you’ll also need to use React.forwardRef in your target component. Without this, the Popup will open at the top-left of the window instead of around the target.

import React from 'react';

import {
  Popup,
  useCloseOnEscape,
  useCloseOnOutsideClick,
  useInitialFocus,
  usePopupModel,
  useReturnFocus,
} from '@workday/canvas-kit-react/popup';
import {px2rem} from '@workday/canvas-kit-styling';

interface MyTargetProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  label: string;
}

const MyTarget = React.forwardRef<HTMLButtonElement, MyTargetProps>(({label, ...props}, ref) => {
  return (
    <button {...props} ref={ref}>
      {label}
    </button>
  );
});

export default () => {
  const model = usePopupModel();

  useCloseOnOutsideClick(model);
  useCloseOnEscape(model);
  useInitialFocus(model);
  useReturnFocus(model);

  return (
    <Popup model={model}>
      <Popup.Target as={MyTarget} label="Open" />
      <Popup.Popper>
        <Popup.Card cs={{minWidth: px2rem(320)}}>
          <Popup.CloseIcon aria-label="Close" />
          <Popup.Heading>Popup</Popup.Heading>
          <Popup.Body>Contents</Popup.Body>
        </Popup.Card>
      </Popup.Popper>
    </Popup>
  );
};

Accessibility Note: Custom targets must be keyboard focusable, otherwise users will not be able to access the popup. Bear in mind that click handlers only work with the keyboard when applied to HTML <button> elements and it is strongly recommended to base your custom target on a <button> element. Otherwise, you will be required to build in your own custom keyboard event handlers for invoking the popup.

Full Screen API

By default, popups are created as children of the document.body element, but the PopupStack supports the Fullscreen API . When fullscreen is entered, the PopupStack will automatically create a new stacking context for all future popups. Any existing popups will disappear, but not be removed. They disappear because the fullscreen API is only showing content within the fullscreen element. There are instances where a popup may not close when fullscreen is exited:

  • The escape key is used to exit fullscreen
  • There is a button to exit fullscreen, but the popup doesn’t use useCloseOnOutsideClick

If fullscreen is exited, popups within the fullscreen stacking context are not removed or transferred automatically. If you do not handle this case, the popup may not render correctly. This example shows a popup that closes when fullscreen is entered/exited and another popup that transfers the popup’s stack context when entering/exiting fullscreen.

import * as React from 'react';
import screenfull from 'screenfull';

import {SecondaryButton} from '@workday/canvas-kit-react/button';
import {useIsFullscreen} from '@workday/canvas-kit-react/common';
import {Flex} from '@workday/canvas-kit-react/layout';
import {
  Popup,
  useCloseOnEscape,
  useCloseOnFullscreenExit,
  useCloseOnOutsideClick,
  useFocusTrap,
  useInitialFocus,
  usePopupModel,
  useReturnFocus,
  useTransferOnFullscreenEnter,
  useTransferOnFullscreenExit,
} from '@workday/canvas-kit-react/popup';
import {px2rem} from '@workday/canvas-kit-styling';
import {system} from '@workday/canvas-tokens-web';

const SelfClosePopup = () => {
  const model = usePopupModel();

  useCloseOnOutsideClick(model);
  useCloseOnEscape(model);
  useInitialFocus(model);
  useReturnFocus(model);
  useFocusTrap(model);
  useCloseOnFullscreenExit(model);

  return (
    <Popup model={model}>
      <Popup.Target>Open Self-close Popup</Popup.Target>
      <Popup.Popper>
        <Popup.Card cs={{width: px2rem(400), padding: system.padding.md}}>
          <Popup.CloseIcon aria-label="Close" />
          <Popup.Heading>Self-close Popup</Popup.Heading>
          <Popup.Body>
            <p>
              When in fullscreen, the escape key will be highjacked by the browser to exit
              fullscreen and <code>useCloseOnEscape</code> hook will not receive the escape key. To
              close when fullscreen is exited, use the <code>useCloseOnFullscreenExit</code> hook.
            </p>
          </Popup.Body>
          <Popup.CloseButton>Close</Popup.CloseButton>
        </Popup.Card>
      </Popup.Popper>
    </Popup>
  );
};

const TransferClosePopup = () => {
  const model = usePopupModel();

  useCloseOnEscape(model);
  useInitialFocus(model);
  useReturnFocus(model);
  useFocusTrap(model);
  useTransferOnFullscreenEnter(model);
  useTransferOnFullscreenExit(model);

  return (
    <Popup model={model}>
      <Popup.Target>Open Transfer Popup</Popup.Target>
      <Popup.Popper>
        <Popup.Card cs={{width: px2rem(400), padding: system.padding.md}}>
          <Popup.CloseIcon aria-label="Close" />
          <Popup.Heading>Transfer Popup</Popup.Heading>
          <Popup.Body>
            <p>
              When in fullscreen, the escape key will be highjacked by the browser to exit
              fullscreen and <code>useCloseOnEscape</code> hook will not receive the escape key. To
              close when fullscreen is exited, use the <code>useTransferOnFullscreenExit</code>{' '}
              hook.
            </p>
          </Popup.Body>
          <Popup.CloseButton>Close</Popup.CloseButton>
        </Popup.Card>
      </Popup.Popper>
    </Popup>
  );
};

export default () => {
  // you could make this a hook depending on which fullscreen library your application uses
  const fullscreenElementRef = React.useRef<HTMLDivElement>();
  const isFullscreen = useIsFullscreen();

  const enterFullScreen = () => {
    screenfull.request(fullscreenElementRef.current);
  };

  const exitFullscreen = () => {
    screenfull.exit();
  };

  return (
    <>
      <SecondaryButton onClick={enterFullScreen}>Open Fullscreen</SecondaryButton>
      <Flex
        ref={fullscreenElementRef}
        cs={{alignItems: 'center', justifyContent: 'center', background: system.color.bg.default}}
      >
        <Flex cs={{gap: system.gap.md}}>
          <SelfClosePopup />
          <TransferClosePopup />
          {isFullscreen ? (
            <SecondaryButton onClick={exitFullscreen}>Exit fullscreen</SecondaryButton>
          ) : null}
        </Flex>
      </Flex>
    </>
  );
};

Opening an External Window

A popup can open an external window. This isn’t supported directly. The Popup.Popper subcomponent is replaced with a custom subcomponent that connects to the Popup model and controls the lifecycle of the extenal window. Be sure to connect the unload event of both the parent window and the external child window to the lifecycle of the Popup model to prevent memory leaks or zombie windows.

Popup that opens a new Operating System Window

Popup visibility: hidden

import React from 'react';
import ReactDOM from 'react-dom';

import {SecondaryButton} from '@workday/canvas-kit-react/button';
import {
  CanvasProvider,
  ContentDirection,
  PartialEmotionCanvasTheme,
  createSubcomponent,
  useMount,
  useTheme,
} from '@workday/canvas-kit-react/common';
import {Flex} from '@workday/canvas-kit-react/layout';
import {Popup, usePopupModel} from '@workday/canvas-kit-react/popup';
import {Tooltip} from '@workday/canvas-kit-react/tooltip';
import {createStyles} from '@workday/canvas-kit-styling';
import {infoIcon} from '@workday/canvas-system-icons-web';
import {system} from '@workday/canvas-tokens-web';

const mainContentStyles = createStyles({
  padding: system.padding.md,
});

export interface ExternalWindowPortalProps {
  /**
   * Child components of WindowPortal
   */
  children: React.ReactNode;
  /**
   * Callback to close the popup
   */
  onWindowClose?: () => void;
  /**
   * Width of the popup globalThis
   */
  width?: number;
  /**
   * Height of the popup globalThis
   */
  height?: number;
  /**
   * The name of the popup globalThis. If another popup opens with the same name, that instance will
   * be reused. Use caution with setting this value
   */
  target?: string;
}

async function copyAssets(sourceDoc: Document, targetDoc: Document) {
  for (const font of (sourceDoc as any).fonts.values()) {
    (targetDoc as any).fonts.add(font);

    font.load();
  }

  await (targetDoc as any).fonts.ready;

  // The current ES lib version doesn't include iterable interfaces, so we cast as an iterable
  for (const styleSheet of sourceDoc.styleSheets as StyleSheetList & Iterable<CSSStyleSheet>) {
    if (styleSheet.cssRules) {
      // text based styles
      const styleEl = targetDoc.createElement('style');
      for (const cssRule of styleSheet.cssRules as CSSRuleList & Iterable<CSSRule>) {
        styleEl.appendChild(targetDoc.createTextNode(cssRule.cssText));
      }
      targetDoc.head.appendChild(styleEl);
    } else if (styleSheet.href) {
      // link based styles
      const linkEl = targetDoc.createElement('link');

      linkEl.rel = 'stylesheet';
      linkEl.href = styleSheet.href;
      targetDoc.head.appendChild(linkEl);
    }
  }
}

const ExternalWindowPortal = ({
  children,
  width = 300,
  height = 500,
  target = '',
  onWindowClose,
}: ExternalWindowPortalProps) => {
  const [portalElement, setPortalElement] = React.useState<HTMLDivElement | null>(null);

  useMount(() => {
    const newWindow = globalThis.open(
      '', // url
      target,
      `width=${width},height=${height},left=100,top=100,popup=true`
    );

    if (newWindow) {
      // copy fonts and styles
      copyAssets(document, newWindow.document);

      const element = newWindow.document.createElement('div');
      newWindow.document.body.appendChild(element);
      setPortalElement(element);
    } else {
      onWindowClose();
    }

    const closeWindow = event => {
      onWindowClose();
    };

    globalThis.addEventListener('unload', closeWindow);
    newWindow?.addEventListener('unload', closeWindow);

    return () => {
      globalThis.removeEventListener('unload', closeWindow);
      newWindow?.removeEventListener('unload', closeWindow);
      newWindow?.close();
    };
  });

  if (!portalElement) {
    return null;
  }

  return ReactDOM.createPortal(<CanvasProvider>{children}</CanvasProvider>, portalElement);
};

const PopupExternalWindow = createSubcomponent()({
  displayName: 'Popup.ExternalWindow',
  modelHook: usePopupModel,
})<ExternalWindowPortalProps>(({children, ...elemProps}, Element, model) => {
  if (model.state.visibility === 'visible') {
    return (
      <ExternalWindowPortal onWindowClose={model.events.hide} {...elemProps}>
        {children}
      </ExternalWindowPortal>
    );
  }

  return null;
});

export default () => {
  // useTheme is filling in the Canvas theme object if any keys are missing
  const canvasTheme: PartialEmotionCanvasTheme = useTheme({
    canvas: {
      // Switch to `ContentDirection.RTL` to change direction
      direction: ContentDirection.LTR,
    },
  });

  const model = usePopupModel();

  return (
    <CanvasProvider theme={canvasTheme}>
      <main className={mainContentStyles}>
        <p>Popup that opens a new Operating System Window</p>
        <Popup model={model}>
          <Tooltip title="Open External Window Tooltip">
            <Popup.Target>Open External Window</Popup.Target>
          </Tooltip>
          <PopupExternalWindow>
            <p>External Window Contents! Mouse over the info icon to get a tooltip</p>
            <Flex cs={{gap: system.gap.sm}}>
              <Tooltip title="More information">
                <SecondaryButton icon={infoIcon} />
              </Tooltip>
              <Popup.CloseButton>Close Window</Popup.CloseButton>
            </Flex>
          </PopupExternalWindow>
        </Popup>
        <p>Popup visibility: {model.state.visibility}</p>
      </main>
    </CanvasProvider>
  );
};

RTL

The Popup component automatically handles right-to-left rendering.

Note: This example shows an inaccessible open card for demonstration purposes.

import {DeleteButton, SecondaryButton} from '@workday/canvas-kit-react/button';
import {CanvasProvider} from '@workday/canvas-kit-react/common';
import {Box, Flex} from '@workday/canvas-kit-react/layout';
import {Popup} from '@workday/canvas-kit-react/popup';
import {px2rem} from '@workday/canvas-kit-styling';
import {system} from '@workday/canvas-tokens-web';

export default () => {
  return (
    <CanvasProvider dir="rtl">
      <Popup.Card cs={{width: px2rem(400)}}>
        <Popup.CloseIcon aria-label="סגור" />
        <Popup.Heading>למחוק פריט</Popup.Heading>
        <Popup.Body>
          <Box as="p" cs={{marginBlock: '0'}}>
            האם ברצונך למחוק פריט זה
          </Box>
        </Popup.Body>
        <Popup.ButtonGroup>
          <SecondaryButton>לְבַטֵל</SecondaryButton>
          <DeleteButton>לִמְחוֹק</DeleteButton>
        </Popup.ButtonGroup>
      </Popup.Card>
    </CanvasProvider>
  );
};

Accessibility

Ensure users of assistive technology can discover, name, and operate a popup that is typically portaled to the end of document.body: the popup has an accessible name that matches its visible heading, keyboard users can open and dismiss it predictably, and focus and reading order remain usable despite portal placement (see Guides > Accessibility > Inline Popups ). Prefer a semantic component before composing Popup directly: Dialog for a standard non-modal dialog (behaviors and aria-owns built in), or Modal for blocking tasks with focus trapping and assistive sibling hiding (see also the W3C Dialog (Modal) Pattern ). Use Popup with composed hooks when you need a custom popup stack or behavior set that those components do not provide.

Minimum Accessible Structure

The following matches the Basic Example: hoist usePopupModel, compose the non-modal behavior hooks on that model, place Popup.CloseIcon before Popup.Heading so default open focus lands on the dismiss control first, and use Popup.CloseButton for actions that should also close the popup.

import {DeleteButton} from '@workday/canvas-kit-react/button'; import { Popup, useCloseOnEscape, useCloseOnOutsideClick, useFocusRedirect, useInitialFocus, usePopupModel, useReturnFocus, } from '@workday/canvas-kit-react/popup'; const Example = () => { const model = usePopupModel(); useCloseOnOutsideClick(model); useCloseOnEscape(model); useInitialFocus(model); useReturnFocus(model); useFocusRedirect(model); return ( <Popup model={model}> <Popup.Target as={DeleteButton}>Delete Item</Popup.Target> <Popup.Popper> <Popup.Card> <Popup.CloseIcon aria-label="Close" /> <Popup.Heading>Delete Item</Popup.Heading> <Popup.Body> <p>Are you sure you'd like to delete the item titled 'My Item'?</p> </Popup.Body> <Popup.ButtonGroup> <Popup.CloseButton>Cancel</Popup.CloseButton> <Popup.CloseButton as={DeleteButton}>Delete</Popup.CloseButton> </Popup.ButtonGroup> </Popup.Card> </Popup.Popper> </Popup> ); };

Include a dismiss control: Popup.CloseButton with visible text (for example “Cancel” or “Close”), and/or Popup.CloseIcon when the design uses an icon-only dismiss (requires aria-label or Tooltip). Pass the same model instance to Popup and every behavior hook so focus and dismiss wiring share one stack.

Built-in Behaviors

Canvas Kit applies ARIA and DOM wiring automatically via Popup subcomponents when you compose them. Behavioral hooks are not applied by usePopupModel alone—you must call them (as in the Basic Example). Once applied, do not duplicate them in consuming code.

Popup behaviors (compose on the model; recommended for non-modal dialogs):

  • useInitialFocus — moves focus into the popup when it opens (default: first focusable element in DOM order; optional override via initialFocusRef on the model)
  • useReturnFocus — returns focus to Popup.Target (or configured return target) when it closes
  • useCloseOnEscape — Escape closes the popup
  • useCloseOnOutsideClick — pointer interaction outside closes the popup
  • useFocusRedirect — Tab / Shift+Tab at the first or last focusable element inside the popup closes it and moves focus to the next or previous focusable element on the page (non-modal; not a focus trap; does not change screen reader reading order; does not provide aria-owns)

ARIA and DOM (applied by hooks/subcomponents):

  • Popup.Card: role="dialog", aria-labelledby referencing the heading id (non-modal by default; page content is not hidden with aria-hidden unless you compose useAssistiveHideSiblings)
  • Popup.Heading: id wired to Popup.Card’s aria-labelledby
  • Popup.Popper: positions and registers the popup with the stack; unlike Dialog.Popper, it does not set aria-owns
  • Popup.CloseIcon / Popup.CloseButton: onClick that calls model.events.hide()
  • Popup.Target: ref and onClick to open and to receive return focus

Implementation note on aria-owns: useFocusRedirect does not provide aria-owns. When you need remapped reading order for portaled content, add it yourself (see Reading order in Accessibility Requirements) or prefer Dialog, which wires aria-owns automatically. Support varies by browser and screen reader.

Keyboard (trigger is Popup.Target, default SecondaryButton):

  • Enter / Space on the trigger opens the popup (standard button behavior)
  • On open and close, focus is managed by useInitialFocus and useReturnFocus when those hooks are composed (application overrides: see Focus management in Accessibility Requirements)
  • Tab / Shift+Tab move focus forward and backward through interactive elements inside the popup (standard sequential focus behavior)
  • With useFocusRedirect, tabbing past the last or before the first focusable element closes the popup
  • Escape closes the popup when useCloseOnEscape is composed and returns focus per useReturnFocus

Screen reader expectations (when built-in behaviors and recommended hooks are used as intended):

  • On open, assistive technology should announce the first focused control (often a dismiss control), the popup name (Popup.Heading), and dialog role
  • Background page content remains available to assistive technology unless you compose useAssistiveHideSiblings (prefer Modal for that pattern)
  • Reading order may still follow document order at the end of body unless aria-owns remapping is added and honored; support varies by browser and screen reader

Accessibility Requirements

Required in application code for an accessible Popup. Always hoist usePopupModel and pass the same instance to Popup and behavior hooks. Rows marked (conditional) apply only when the situation matches—otherwise omit.

If no design spec is provided: compose the Basic Example hooks (useCloseOnOutsideClick, useCloseOnEscape, useInitialFocus, useReturnFocus, useFocusRedirect); use default focus behavior; omit initialFocusRef, returnFocusRef, aria-describedby, aria-expanded, aria-haspopup, useFocusTrap, and useAssistiveHideSiblings. Prefer Dialog or Modal when those components already match the product need.

Focus management — defaults and developer prompts: When useInitialFocus / useReturnFocus are composed, Canvas Kit handles open and close focus automatically. State the default to the developer first. Only set initialFocusRef or returnFocusRef after the developer (or an explicit design spec) chooses a non-default target. Do not generate focus refs by default.

WhenDefault behaviorAsk the developer before overriding
Popup opensuseInitialFocus moves focus to the first focusable element in DOM order inside the popup (often Popup.CloseIcon or Popup.CloseButton). Omit initialFocusRef.Which element should receive focus when the popup opens? (Only when the default first focusable element is wrong for the design.) Attach initialFocusRef to that element on usePopupModel.
Popup closesuseReturnFocus moves focus to Popup.Target. Omit returnFocusRef.Which element should receive focus when the popup closes? (Only when return focus should land somewhere other than Popup.Target.)

If close removes the trigger from the DOM, returnFocusRef alone is not enough—move focus after the UI updates (for example with useLayoutEffect). See Modal > Return Focus .

Custom targets (conditional): Apply when using a custom as component on Popup.Target. Popup.Target adds onClick and ref. Custom targets must forward both to a keyboard-focusable element (prefer a native <button> or as={SecondaryButton} / another Canvas Kit button). Wrap the component in React.forwardRef when it does not forward refs by default (required if the popup can open programmatically before the user clicks the target).

Reading order (aria-owns) (conditional):

Popup content is portaled; useFocusRedirect alone does not fix screen reader reading order. When a design needs remapped sequential reading order and you are not using Dialog, set an id on the stack element and point a sibling element’s aria-owns at that id (see the Focus Redirect example). Prefer Dialog when that pattern is the product default—Dialog wires aria-owns for you.

Modal-like focus trapping (conditional):

Prefer Modal for blocking tasks. If you must compose trapping on Popup, use useFocusTrap with useAssistiveHideSiblings (and typically omit useFocusRedirect). Focus trapping does not stop mouse or virtual-cursor escape by itself.

Open focus below the heading (conditional; see supplementary copy row below):

Button-focus variant (matches Initial Focus): when open focus lands on a primary action below the heading, wire aria-describedby to the supplementary copy. For the form-field variant (focus an input), see Dialog  or Modal  Open focus below the heading.

import React from 'react'; import {PrimaryButton} from '@workday/canvas-kit-react/button'; import {useUniqueId} from '@workday/canvas-kit-react/common'; import { Popup, useCloseOnEscape, useCloseOnOutsideClick, useFocusRedirect, useInitialFocus, usePopupModel, useReturnFocus, } from '@workday/canvas-kit-react/popup'; const Example = () => { const messageId = useUniqueId(); const initialFocusRef = React.useRef(null); const model = usePopupModel({initialFocusRef}); useCloseOnOutsideClick(model); useCloseOnEscape(model); useInitialFocus(model); useReturnFocus(model); useFocusRedirect(model); return ( <Popup model={model}> <Popup.Target>Open</Popup.Target> <Popup.Popper> <Popup.Card aria-describedby={messageId}> <Popup.Heading>Confirmation</Popup.Heading> <Popup.Body> <p id={messageId}>Your message has been sent!</p> </Popup.Body> <Popup.ButtonGroup> <Popup.CloseButton as={PrimaryButton} ref={initialFocusRef}> OK </Popup.CloseButton> </Popup.ButtonGroup> </Popup.Card> </Popup.Popper> </Popup> ); };

When open focus lands on Popup.Heading itself, add tabIndex={-1} so the heading can receive programmatic focus.

RequirementHow to satisfy
Shared model + behavior hooksHoist usePopupModel, pass model={model} to Popup, and compose at least the Basic Example hooks for non-modal dialogs (useCloseOnOutsideClick, useCloseOnEscape, useInitialFocus, useReturnFocus, useFocusRedirect) unless a design deliberately omits one.
Accessible popup nameUse Popup.Heading so aria-labelledby on Popup.Card references a visible title. Do not omit the heading: Popup.Card always sets aria-labelledby, and an aria-label fallback is unreliable when that ID does not exist.
Dismiss controlProvide a way to close the popup: Popup.CloseButton with visible text (no extra aria-label needed), and/or Popup.CloseIcon for icon-only dismiss (requires Tooltip or translated aria-label).
Keyboard-operable triggerSee Custom targets above.
Supplementary copy when overriding open focus (conditional)When initialFocusRef places open focus below Popup.Heading, assign a unique id to supplementary text and pass aria-describedby on Popup.Card. See Open focus below the heading above and Initial Focus.
Reading order remapping (conditional)See Reading order (aria-owns) above, or use Dialog.
Focus trapping / hide siblings (conditional)Prefer Modal. If composing on Popup, see Modal-like focus trapping above.
Open/closed state on the trigger (conditional)See Wiring aria-expanded below. Default: omit aria-expanded and aria-haspopup.

Summary for code generation:

  • REQUIRED: shared usePopupModel, non-modal behavior hooks (unless design omits), accessible name, dismiss control, keyboard-operable trigger
  • CONDITIONAL: initialFocusRef, returnFocusRef, aria-describedby, tabIndex={-1} on heading focus, aria-owns, useFocusTrap / useAssistiveHideSiblings, aria-expanded / aria-haspopup, forwardRef on custom Popup.Target

Wiring aria-expanded (conditional):

The aria-expanded pattern is uncommon for dialog-like Popups—omit aria-expanded and aria-haspopup unless a review deliberately keeps open focus on the trigger (for example initialFocusRef on the trigger per design spec). When required, on Popup.Target set aria-expanded={model.state.visibility !== 'hidden'} and aria-haspopup="dialog". See Focus management and the open/closed-state row above.

Anti-Patterns

Do not generate code that does the following (see Accessibility Requirements above for what to supply instead):

  • Manually set role="dialog", aria-labelledby, or the heading id on Popup.Card or Popup.Heading — Canvas Kit hooks wire these
  • Call behavior hooks on a different model instance than the one passed to Popup, or omit model={model} after composing hooks outside the container
  • Assume usePopupModel alone provides focus, escape, outside-click, or redirect behaviors — compose the hooks (or use Dialog / Modal)
  • Omit Popup.Popper, render Popup.Card outside it, or add a custom portal/restructure instead of Popup → Popup.Popper → Popup.Card without using usePopupStack
  • Use open / onClose props on Popup — Popup has no controlled visibility props; use usePopupModel and model.events.show() / model.events.hide()
  • Reach for Popup + useFocusTrap / useAssistiveHideSiblings when Modal already matches the product need, or for non-modal UX when Dialog already matches
  • Set initialFocusRef or returnFocusRef by default — state the default focus behavior first and ask the developer before overriding (see Focus management in Accessibility Requirements)
  • Add aria-expanded / aria-haspopup on the default dialog-like Popup path, or bind aria-expanded to a static value (see Wiring aria-expanded in Accessibility Requirements)
  • Use a custom Popup.Target as component that does not forward ref to a focusable element — use React.forwardRef or a Canvas Kit button component instead
  • Rely on returnFocusRef alone when close removes the trigger from the DOM (see Modal > Return Focus )
  • Nest multiple Popup instances without deliberate initial focus and return-focus planning
  • Assume useFocusRedirect fixes screen reader reading order, or that aria-owns remapping works in all browser and screen reader combinations — test your supported combinations
  • Expect Popup.Popper to set aria-owns like Dialog.Popper — it does not