Popup
Custom popups communicate relevant and timely information to users in response to user action or through system-generated messages.
Component Type
Popup
Platform
Web
Component
Sana Canvas
Delivery Channels
Web
Version
16.1.7Experience Surfaces
Page Body Inline
Anatomy

- Title (Optional): Titles should display the title of the content or dialog.
- Content: Popups contain different types of content. Typical types of content include alerts and dialogs.
- Buttons(Optional): When there is a user action, use the action bar. When displaying informational content, use in-line buttons.
- 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
idto supplementary text and passaria-describedbyonPopup.Card. This augments the includedaria-labelledbyreference toPopup.Headingso screen readers can announce both the heading and any supplementary text automatically. When initial focus is on the heading itself, addtabIndex={-1}toPopup.Headingso 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
useFocusRedirecthook 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 ofaria-ownson a sibling<div>element pointing to thePopup.Cardcomponent. 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.forwardRefin 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 viainitialFocusRefon the model)useReturnFocus— returns focus toPopup.Target(or configured return target) when it closesuseCloseOnEscape— Escape closes the popupuseCloseOnOutsideClick— pointer interaction outside closes the popupuseFocusRedirect— 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 providearia-owns)
ARIA and DOM (applied by hooks/subcomponents):
Popup.Card:role="dialog",aria-labelledbyreferencing the headingid(non-modal by default; page content is not hidden witharia-hiddenunless you composeuseAssistiveHideSiblings)Popup.Heading:idwired toPopup.Card’saria-labelledbyPopup.Popper: positions and registers the popup with the stack; unlike Dialog.Popper, it does not setaria-ownsPopup.CloseIcon/Popup.CloseButton:onClickthat callsmodel.events.hide()Popup.Target:refandonClickto 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
useInitialFocusanduseReturnFocuswhen 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
useCloseOnEscapeis composed and returns focus peruseReturnFocus
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), anddialogrole - 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
bodyunlessaria-ownsremapping 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.
| When | Default behavior | Ask the developer before overriding |
|---|---|---|
| Popup opens | useInitialFocus 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 closes | useReturnFocus 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.
| Requirement | How to satisfy |
|---|---|
| Shared model + behavior hooks | Hoist 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 name | Use 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 control | Provide 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 trigger | See 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,forwardRefon customPopup.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 headingidonPopup.CardorPopup.Heading— Canvas Kit hooks wire these - Call behavior hooks on a different model instance than the one passed to
Popup, or omitmodel={model}after composing hooks outside the container - Assume
usePopupModelalone provides focus, escape, outside-click, or redirect behaviors — compose the hooks (or use Dialog / Modal) - Omit
Popup.Popper, renderPopup.Cardoutside it, or add a custom portal/restructure instead ofPopup→Popup.Popper→Popup.Cardwithout usingusePopupStack - Use
open/onCloseprops onPopup— Popup has no controlled visibility props; useusePopupModelandmodel.events.show()/model.events.hide() - Reach for Popup +
useFocusTrap/useAssistiveHideSiblingswhen Modal already matches the product need, or for non-modal UX when Dialog already matches - Set
initialFocusReforreturnFocusRefby default — state the default focus behavior first and ask the developer before overriding (see Focus management in Accessibility Requirements) - Add
aria-expanded/aria-haspopupon the default dialog-like Popup path, or bindaria-expandedto a static value (see Wiring aria-expanded in Accessibility Requirements) - Use a custom
Popup.Targetascomponent that does not forwardrefto a focusable element — useReact.forwardRefor a Canvas Kit button component instead - Rely on
returnFocusRefalone 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
useFocusRedirectfixes screen reader reading order, or thataria-ownsremapping works in all browser and screen reader combinations — test your supported combinations - Expect
Popup.Popperto setaria-ownslike Dialog.Popper — it does not