Modal
Modals are interactive pop-ups reserved for situations which require immediate attention.
Component Type
Popup
Platform
Web
Component
Sana Canvas
Delivery Channels
Web
Version
16.1.7Experience Surfaces
Page Body Inline
Anatomy

- Card: The Card contains content for a modal. It has no stroke or depth applied because it appears in front of an overlay.
- Heading (Optional): Heading should display the title of the content or task.
- Body: Modals contain many different types of content in the body. Typical types of content include media, alerts, dialogs, and/or task-oriented flows.
- In-line Buttons (Optional): Action should be at the bottom of the container when used. There are multiple alignments available for use; Left (Default), Center, Full Width & Full Width Stacked, or Right aligned.
- Close “X” Icon (Optional): Users must be able to intentionally dismiss a modal. This icon inherits styling and interactions from our Tertiary Icon-Only Button Variant.
- Overlay: Used to block user interaction with content behind it. When there are no Close “X” Icon, clicking on the overlay doesn’t dismiss the modal.
Usage Guidance
- Modals allow for entry of data or alert users on any given page after an action has been initiated and require immediate attention.
- On web platforms with browser windows wider than 766px, Modals show up in the center of the screen and in front of an overlay.
- On web platforms with browser windows less than 767px width, Dialogs show up at the bottom of the screen and in front of an overlay.
- In-line buttons used in modal dialogs and non-user input modals, the alignment could be Left (Default), Center, Full Width & Full Width Stacked, or Right aligned.
When to Use
- Use Modal to gather immediate input from the user by blocking interaction with the rest of the page.
- Use Modal when alert content and text are too large for a standard Toast or Pop-up notification.
When to Use Something Else
- Consider a Dialog to gather non-critical input from the user without blocking interaction with the rest of the page.
- Do not use Modals to serve up easily accessible links or simple messages that can be dismissed quickly (use Toasts or Popups for this).
- Do not use Modals to display dense information, such as Tables or Multi-View Containers.
- 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.
Responsive View
Modal components adjust width and content presentation based on screen size. When content exceeds the length of the screen, the modal content will become scrollable in the body section of the modal. For long content on a small screen, inline buttons will continue to scroll with the content.
Touch Based Behavior
The overlay on modals are not click or touch enabled to close the modal component view on small screens between 320-767px. This accounts for accidental touch on mobile devices. Background overlays will close the modal when clicked on larger devices when the screen reaches the minimum width.
Examples
Basic Example
The basic behavior of a modal is to hide all content from all users that is “behind” the modal dialog.
import {PrimaryButton} from '@workday/canvas-kit-react/button';
import {Box} from '@workday/canvas-kit-react/layout';
import {Modal} from '@workday/canvas-kit-react/modal';
export default () => {
const handleAcknowledge = () => {
console.log('License Acknowledged');
};
const handleCancel = () => {
console.log('Cancel clicked');
};
return (
<Modal>
<Modal.Target as={PrimaryButton}>Open License</Modal.Target>
<Modal.Overlay>
<Modal.Card>
<Modal.CloseIcon aria-label="Close" />
<Modal.Heading>MIT License</Modal.Heading>
<Modal.Body>
<Box as="p" cs={{marginBlock: '0'}}>
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software").
</Box>
</Modal.Body>
<Modal.ButtonGroup>
<Modal.CloseButton onClick={handleCancel}>Cancel</Modal.CloseButton>
<Modal.CloseButton as={PrimaryButton} onClick={handleAcknowledge}>
Acknowledge
</Modal.CloseButton>
</Modal.ButtonGroup>
</Modal.Card>
</Modal.Overlay>
</Modal>
);
};
Without Close Icon
If you wish to remove the close icon button, you can simply omit the Modal.CloseIcon subcomponent.
If you have a modal dialog that requires the user to accept instead of dismiss through an escape key
or clicking outside the modal, you must create a new PopupModel without those behaviors and hand
that model to the Modal dialog component.
import React from 'react';
import {DeleteButton} from '@workday/canvas-kit-react/button';
import {useUniqueId} from '@workday/canvas-kit-react/common';
import {Box} from '@workday/canvas-kit-react/layout';
import {Modal} from '@workday/canvas-kit-react/modal';
import {
useAssistiveHideSiblings,
useDisableBodyScroll,
useFocusTrap,
useInitialFocus,
usePopupModel,
useReturnFocus,
} from '@workday/canvas-kit-react/popup';
export default () => {
const longDescId = useUniqueId();
const cancelBtnRef = React.useRef(null);
const model = usePopupModel({
initialFocusRef: cancelBtnRef,
});
// disable useCloseOnEscape and useCloseOnOverlayClick
useInitialFocus(model);
useReturnFocus(model);
useFocusTrap(model);
useAssistiveHideSiblings(model);
useDisableBodyScroll(model);
const handleDelete = () => {
console.log('Deleted item');
};
return (
<Modal model={model}>
<Modal.Target as={DeleteButton}>Delete Item</Modal.Target>
<Modal.Overlay>
<Modal.Card aria-describedby={longDescId}>
<Modal.Heading>Delete Item</Modal.Heading>
<Modal.Body>
<Box as="p" id={longDescId} cs={{marginBlock: '0'}}>
Are you sure you want to delete the item?
</Box>
</Modal.Body>
<Modal.ButtonGroup>
<Modal.CloseButton ref={cancelBtnRef}>Cancel</Modal.CloseButton>
<Modal.CloseButton as={DeleteButton} onClick={handleDelete}>
Delete
</Modal.CloseButton>
</Modal.ButtonGroup>
</Modal.Card>
</Modal.Overlay>
</Modal>
);
};
Custom Focus
By default, the Modal makes sure the first focusable element receives focus when the Modal is
opened. Most of the time, this is the Modal.CloseIcon button. If that element isn’t present, the
Modal will use the Modal Heading to make sure screen reader users have focus near the start of the
Modal’s content. This allows screen reader users to discover the Modal’s content more naturally
without having to navigate back up again. Sometimes, it is a better user experience to focus on a
different element. The following example shows how initialFocusRef can be used to change which
element receives focus when the modal opens.
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 {Box} from '@workday/canvas-kit-react/layout';
import {Modal, useModalModel} from '@workday/canvas-kit-react/modal';
import {TextInput} from '@workday/canvas-kit-react/text-input';
import {system} from '@workday/canvas-tokens-web';
export default () => {
const longDescID = useUniqueId();
const ref = React.useRef<HTMLInputElement>(null);
const [value, setValue] = React.useState('');
const model = useModalModel({
initialFocusRef: ref,
});
const handleAcknowledge = () => {
console.log('Acknowledged license');
};
return (
<Modal model={model}>
<Modal.Target as={PrimaryButton}>Acknowledge License</Modal.Target>
<Modal.Overlay>
<Modal.Card aria-describedby={longDescID}>
<Modal.CloseIcon aria-label="Close" />
<Modal.Heading>Acknowledge License</Modal.Heading>
<Modal.Body>
<Box as="p" id={longDescID} cs={{marginBlockStart: 0, marginBlockEnd: system.gap.md}}>
Enter your initials to acknowledge the license.
</Box>
<FormField>
<FormField.Label>Initials</FormField.Label>
<FormField.Input
as={TextInput}
ref={ref}
value={value}
grow
onChange={e => setValue(e.currentTarget.value)}
/>
</FormField>
</Modal.Body>
<Modal.ButtonGroup>
<Modal.CloseButton>Cancel</Modal.CloseButton>
<Modal.CloseButton as={PrimaryButton} onClick={handleAcknowledge}>
Acknowledge
</Modal.CloseButton>
</Modal.ButtonGroup>
</Modal.Card>
</Modal.Overlay>
</Modal>
);
};
Accessibility Note: When initial focus lands on a control below the heading (for example, a text field instead of the close button), give supplementary copy a unique
idand passaria-describedbyonModal.Cardso screen readers can announce both the dialog name and that text. For more examples of custom focus techniques, see Popup > Initial Focus .
Return Focus
By default, the Modal will return focus to the Modal.Target element. When you open the modal with
model.events.show() (without Modal.Target), set returnFocusRef on the model to the element
that should receive focus when the modal closes—for example the button that opened it. That covers
cancel, Escape, and the close icon: focus returns to the control the user activated.
If confirming an action removes that control from the document (such as deleting the row that
held the delete button), returnFocusRef alone cannot land on a new target. The example below
uses useLayoutEffect after the list updates to move focus to another row’s delete control, or
to empty-state text when no files remain.
Uploaded Files
- Resume.docx
- Cover_Letter.docx
- References.docx
import React from 'react';
import {DeleteButton} from '@workday/canvas-kit-react/button';
import {useUniqueId} from '@workday/canvas-kit-react/common';
import {Box, Flex} from '@workday/canvas-kit-react/layout';
import {Modal, useModalModel} from '@workday/canvas-kit-react/modal';
import {Heading, Text} from '@workday/canvas-kit-react/text';
import {Tooltip} from '@workday/canvas-kit-react/tooltip';
import {createStyles} from '@workday/canvas-kit-styling';
import {trashIcon} from '@workday/canvas-system-icons-web';
import {system} from '@workday/canvas-tokens-web';
const INITIAL_FILES = ['Resume.docx', 'Cover_Letter.docx', 'References.docx'];
const headingStyles = createStyles({
marginBlock: '0',
});
const emptyStateStyles = createStyles({
maxWidth: '28rem',
outline: 'none',
});
const listStyles = createStyles({
flexDirection: 'column',
gap: system.gap.md,
marginBlock: '0',
padding: '0',
listStyle: 'none',
maxWidth: '28rem',
});
const rowStyles = createStyles({
alignItems: 'center',
justifyContent: 'space-between',
gap: system.gap.md,
width: '100%',
});
function fileNameId(name: string) {
return `return-focus-file-${name.replace(/[^a-zA-Z0-9]/g, '_')}`;
}
/** Index of a delete button to focus after removing `deletedIndex`, or empty list. */
function nextListFocusAfterDelete(deletedIndex: number, lengthBeforeDelete: number) {
if (lengthBeforeDelete <= 1) {
return 'empty' as const;
}
return deletedIndex < lengthBeforeDelete - 1 ? deletedIndex : deletedIndex - 1;
}
export default () => {
const [items, setItems] = React.useState<string[]>(() => [...INITIAL_FILES]);
const [confirmingFileName, setConfirmingFileName] = React.useState<string | null>(null);
const bodyTextId = useUniqueId();
const returnFocusRef = React.useRef<HTMLButtonElement | null>(null);
const cancelButtonRef = React.useRef<HTMLButtonElement>(null);
const deleteButtonRefs = React.useRef<(HTMLButtonElement | null)[]>([]);
const emptyStateRef = React.useRef<HTMLDivElement>(null);
const pendingDeleteIndexRef = React.useRef<number | null>(null);
const postDeleteFocusRef = React.useRef<number | 'empty' | null>(null);
const model = useModalModel({
returnFocusRef,
initialFocusRef: cancelButtonRef,
});
React.useEffect(() => {
if (model.state.visibility === 'hidden') {
setConfirmingFileName(null);
pendingDeleteIndexRef.current = null;
}
}, [model.state.visibility]);
React.useLayoutEffect(() => {
if (postDeleteFocusRef.current === null) {
return;
}
if (postDeleteFocusRef.current === 'empty') {
emptyStateRef.current?.focus();
} else {
deleteButtonRefs.current[postDeleteFocusRef.current]?.focus();
}
postDeleteFocusRef.current = null;
}, [items]);
const openDeleteModal = (index: number) => {
pendingDeleteIndexRef.current = index;
setConfirmingFileName(items[index]);
returnFocusRef.current = deleteButtonRefs.current[index];
model.events.show();
};
const handleConfirmDelete = () => {
const idx = pendingDeleteIndexRef.current;
if (idx === null) {
return;
}
postDeleteFocusRef.current = nextListFocusAfterDelete(idx, items.length);
pendingDeleteIndexRef.current = null;
setItems(prev => prev.filter((_, i) => i !== idx));
};
return (
<Modal model={model}>
<Heading as="h4" size="small" cs={headingStyles}>
Uploaded Files
</Heading>
<Box>
{items.length > 0 ? (
<Flex as="ul" cs={listStyles}>
{items.map((name, index) => (
<Flex as="li" key={name} cs={rowStyles}>
<Text as="span" id={fileNameId(name)}>
{name}
</Text>
<Tooltip title="Delete">
<DeleteButton
aria-describedby={fileNameId(name)}
icon={trashIcon}
ref={el => {
deleteButtonRefs.current[index] = el;
}}
onClick={() => openDeleteModal(index)}
/>
</Tooltip>
</Flex>
))}
</Flex>
) : (
<Box ref={emptyStateRef} tabIndex={-1} cs={emptyStateStyles}>
<Text>No files remaining.</Text>
</Box>
)}
</Box>
<Modal.Overlay>
<Modal.Card aria-describedby={bodyTextId}>
<Modal.Heading>Delete file?</Modal.Heading>
<Modal.Body>
<Text id={bodyTextId}>
{confirmingFileName
? `Are you sure you want to delete ${confirmingFileName}?`
: 'Are you sure you want to delete this file?'}
</Text>
</Modal.Body>
<Modal.ButtonGroup>
<Modal.CloseButton ref={cancelButtonRef}>Cancel</Modal.CloseButton>
<Modal.CloseButton as={DeleteButton} onClick={handleConfirmDelete}>
Delete
</Modal.CloseButton>
</Modal.ButtonGroup>
</Modal.Card>
</Modal.Overlay>
</Modal>
);
};
Accessibility Note: After an item is deleted, focus is returned to the next item in the list or to the empty state text when no items remain.
Custom Target
It is common to have a custom target for your modal. Use the as prop to use your custom component.
The Modal.Target element will add onClick and ref to the provided component. Your provided
target component must forward the onClick to an element for the Modal to open. The as will cause
Modal.Target to inherit the interface of your custom target component. This means any props your
target requires, Modal.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 Modal without the user interacting with the target button first, you’ll also need to use
React.forwardRefin your target component. Without this, the Modal will open at the top-left of the window instead of around the target.
import React from 'react';
import {Modal} from '@workday/canvas-kit-react/modal';
interface MyTargetProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
label: string;
}
const MyTarget = ({label, ...props}: MyTargetProps) => {
return <button {...props}>{label}</button>;
};
export default () => {
return (
<Modal>
<Modal.Target as={MyTarget} label="Open" />
<Modal.Overlay>
<Modal.Card>
<Modal.CloseIcon aria-label="Close" />
<Modal.Heading>Modal Heading</Modal.Heading>
<Modal.Body>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec semper facilisis dolor
quis facilisis. Aenean tempor eget quam et semper. Nam malesuada rhoncus euismod.
Quisque vel urna feugiat, dictum risus sed, pulvinar nulla. Sed gravida, elit non
iaculis blandit, ligula tortor posuere mauris, vitae cursus turpis nunc non arcu.
</Modal.Body>
</Modal.Card>
</Modal.Overlay>
</Modal>
);
};
Accessibility Note: Custom targets must be keyboard focusable, otherwise users will not be able to access the modal. 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 modal.
Body Content Overflow
The Modal automatically handles overflowing content inside the Modal.Body element. If contents are
larger than the browser’s height will allow, the content will overflow with a scrollbar. You may
need to restrict the height of your browser to observe the overflow.
import {PrimaryButton} from '@workday/canvas-kit-react/button';
import {Flex} from '@workday/canvas-kit-react/layout';
import {Modal} from '@workday/canvas-kit-react/modal';
import {system} from '@workday/canvas-tokens-web';
export default () => {
const handleAcknowledge = () => {
console.log('License Acknowledged');
};
const handleCancel = () => {
console.log('Cancel clicked');
};
return (
<Modal>
<Modal.Target as={PrimaryButton}>Open License</Modal.Target>
<Modal.Overlay>
<Modal.Card>
<Modal.CloseIcon aria-label="Close" />
<Modal.Heading>MIT License</Modal.Heading>
<Modal.Body tabIndex={0}>
<p style={{marginBlockStart: 0}}>
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following
conditions:
</p>
<p>
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
</p>
<p>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Amet massa vitae tortor condimentum
lacinia quis. Fermentum odio eu feugiat pretium nibh ipsum consequat nisl. Sed lectus
vestibulum mattis ullamcorper velit sed. Rutrum tellus pellentesque eu tincidunt
tortor aliquam nulla. Vitae turpis massa sed elementum tempus egestas sed sed risus.
Cursus vitae congue mauris rhoncus aenean vel elit scelerisque mauris. Id neque
aliquam vestibulum morbi blandit cursus risus at. Vel eros donec ac odio tempor orci.
Ac felis donec et odio pellentesque diam volutpat. Laoreet non curabitur gravida arcu
ac tortor dignissim. Rhoncus urna neque viverra justo nec ultrices dui. Bibendum arcu
vitae elementum curabitur vitae nunc sed velit dignissim. Sed risus pretium quam
vulputate dignissim suspendisse in est. Curabitur gravida arcu ac tortor. Nam libero
justo laoreet sit amet cursus sit amet. Arcu dui vivamus arcu felis bibendum ut
tristique et egestas. Eros donec ac odio tempor orci dapibus ultrices. At erat
pellentesque adipiscing commodo elit at. Dignissim cras tincidunt lobortis feugiat
vivamus at augue.
</p>
<p>
Amet commodo nulla facilisi nullam vehicula ipsum. Blandit libero volutpat sed cras.
Quam lacus suspendisse faucibus interdum posuere. Aenean euismod elementum nisi quis
eleifend. Orci nulla pellentesque dignissim enim sit amet venenatis. Diam vel quam
elementum pulvinar etiam non quam lacus. Sit amet dictum sit amet justo donec enim
diam vulputate. Tincidunt ornare massa eget egestas purus. Pulvinar neque laoreet
suspendisse interdum consectetur libero id faucibus. Morbi tincidunt augue interdum
velit. Nullam non nisi est sit amet.
</p>
<p style={{marginBlockEnd: 0}}>
Aliquet enim tortor at auctor urna nunc id cursus metus. Leo urna molestie at
elementum eu facilisis. Consectetur purus ut faucibus pulvinar elementum integer.
Volutpat est velit egestas dui id ornare arcu odio. At consectetur lorem donec massa
sapien. Condimentum vitae sapien pellentesque habitant. Pellentesque habitant morbi
tristique senectus. Et molestie ac feugiat sed lectus vestibulum. Arcu risus quis
varius quam quisque. Turpis massa tincidunt dui ut ornare lectus sit amet. Magna eget
est lorem ipsum dolor sit. Suspendisse faucibus interdum posuere lorem ipsum. Nisi
vitae suscipit tellus mauris a diam maecenas sed. Ipsum dolor sit amet consectetur
adipiscing. Ultricies integer quis auctor elit sed. Scelerisque varius morbi enim nunc
faucibus a. Tortor consequat id porta nibh venenatis cras. Consectetur adipiscing elit
ut aliquam purus sit.
</p>
</Modal.Body>
<Modal.ButtonGroup>
<Modal.CloseButton onClick={handleCancel}>Cancel</Modal.CloseButton>
<Modal.CloseButton as={PrimaryButton} onClick={handleAcknowledge}>
Acknowledge
</Modal.CloseButton>
</Modal.ButtonGroup>
</Modal.Card>
</Modal.Overlay>
</Modal>
);
};
Accessibility Note: When body content overflows, ensure users can scroll that region using only the keyboard. Mouse users can drag scrollbars, but keyboard users need another path. In this example,
tabIndex={0}is set onModal.Bodyso the scrollable area can receive focus; once focused, arrow keys move the viewport within the overflowing content.
Full overlay scrolling
If content is large, scrolling the entire overlay container is an option. Use the
Modal.OverflowOverlay component instead of the Modal.Overlay component. The Modal.Card’s
maxHeight and height will need to be reset to inherit to prevent any internal overflow.
This has the effect of scrolling the heading, close button, and any action buttons. If this type of scrolling behavior is not desired, try the Body Content Overflow method.
import {PrimaryButton} from '@workday/canvas-kit-react/button';
import {Flex} from '@workday/canvas-kit-react/layout';
import {Modal} from '@workday/canvas-kit-react/modal';
import {system} from '@workday/canvas-tokens-web';
export default () => {
const handleAcknowledge = () => {
console.log('License Acknowledged');
};
const handleCancel = () => {
console.log('Cancel clicked');
};
return (
<Modal>
<Modal.Target as={PrimaryButton}>Open License</Modal.Target>
<Modal.OverflowOverlay>
<Modal.Card cs={{maxHeight: 'inherit', height: 'inherit'}}>
<Modal.CloseIcon aria-label="Close" />
<Modal.Heading>MIT License</Modal.Heading>
<Modal.Body tabIndex={0}>
<p style={{marginBlockStart: 0}}>
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following
conditions:
</p>
<p>
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
</p>
<p>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Amet massa vitae tortor condimentum
lacinia quis. Fermentum odio eu feugiat pretium nibh ipsum consequat nisl. Sed lectus
vestibulum mattis ullamcorper velit sed. Rutrum tellus pellentesque eu tincidunt
tortor aliquam nulla. Vitae turpis massa sed elementum tempus egestas sed sed risus.
Cursus vitae congue mauris rhoncus aenean vel elit scelerisque mauris. Id neque
aliquam vestibulum morbi blandit cursus risus at. Vel eros donec ac odio tempor orci.
Ac felis donec et odio pellentesque diam volutpat. Laoreet non curabitur gravida arcu
ac tortor dignissim. Rhoncus urna neque viverra justo nec ultrices dui. Bibendum arcu
vitae elementum curabitur vitae nunc sed velit dignissim. Sed risus pretium quam
vulputate dignissim suspendisse in est. Curabitur gravida arcu ac tortor. Nam libero
justo laoreet sit amet cursus sit amet. Arcu dui vivamus arcu felis bibendum ut
tristique et egestas. Eros donec ac odio tempor orci dapibus ultrices. At erat
pellentesque adipiscing commodo elit at. Dignissim cras tincidunt lobortis feugiat
vivamus at augue.
</p>
<p>
Amet commodo nulla facilisi nullam vehicula ipsum. Blandit libero volutpat sed cras.
Quam lacus suspendisse faucibus interdum posuere. Aenean euismod elementum nisi quis
eleifend. Orci nulla pellentesque dignissim enim sit amet venenatis. Diam vel quam
elementum pulvinar etiam non quam lacus. Sit amet dictum sit amet justo donec enim
diam vulputate. Tincidunt ornare massa eget egestas purus. Pulvinar neque laoreet
suspendisse interdum consectetur libero id faucibus. Morbi tincidunt augue interdum
velit. Nullam non nisi est sit amet.
</p>
<p style={{marginBlockEnd: 0}}>
Aliquet enim tortor at auctor urna nunc id cursus metus. Leo urna molestie at
elementum eu facilisis. Consectetur purus ut faucibus pulvinar elementum integer.
Volutpat est velit egestas dui id ornare arcu odio. At consectetur lorem donec massa
sapien. Condimentum vitae sapien pellentesque habitant. Pellentesque habitant morbi
tristique senectus. Et molestie ac feugiat sed lectus vestibulum. Arcu risus quis
varius quam quisque. Turpis massa tincidunt dui ut ornare lectus sit amet. Magna eget
est lorem ipsum dolor sit. Suspendisse faucibus interdum posuere lorem ipsum. Nisi
vitae suscipit tellus mauris a diam maecenas sed. Ipsum dolor sit amet consectetur
adipiscing. Ultricies integer quis auctor elit sed. Scelerisque varius morbi enim nunc
faucibus a. Tortor consequat id porta nibh venenatis cras. Consectetur adipiscing elit
ut aliquam purus sit.
</p>
</Modal.Body>
<Modal.ButtonGroup>
<Modal.CloseButton onClick={handleCancel}>Cancel</Modal.CloseButton>
<Modal.CloseButton as={PrimaryButton} onClick={handleAcknowledge}>
Acknowledge
</Modal.CloseButton>
</Modal.ButtonGroup>
</Modal.Card>
</Modal.OverflowOverlay>
</Modal>
);
};
Form Modal
The Modal.Card can be turned into a form element to make a form modal. The model should be
hoisted to allow for form validation and allow you to control when the modal closes.
import React from 'react';
import {PrimaryButton} from '@workday/canvas-kit-react/button';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {Modal, useModalModel} from '@workday/canvas-kit-react/modal';
import {Select} from '@workday/canvas-kit-react/select';
import {TextInput} from '@workday/canvas-kit-react/text-input';
import {plusIcon} from '@workday/canvas-system-icons-web';
const FAVORITE_COLOR_OPTIONS = ['Blue', 'Yellow'];
export default () => {
const model = useModalModel();
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // prevent a page reload
// do form validation here
console.log('form data', {
first: (event.currentTarget.elements.namedItem('first') as HTMLInputElement).value,
last: (event.currentTarget.elements.namedItem('last') as HTMLInputElement).value,
favoriteColor: (event.currentTarget.elements.namedItem('favoriteColor') as HTMLInputElement)
.value,
});
// if it looks good, submit to the server and close the modal
model.events.hide();
};
return (
<Modal model={model}>
<Modal.Target icon={plusIcon}>Create New User</Modal.Target>
<Modal.Overlay>
<Modal.Card as="form" onSubmit={onSubmit}>
<Modal.CloseIcon aria-label="Close" />
<Modal.Heading>New User</Modal.Heading>
<Modal.Body>
<FormField grow>
<FormField.Label>First Name</FormField.Label>
<FormField.Input as={TextInput} name="first" />
</FormField>
<FormField grow>
<FormField.Label>Last Name</FormField.Label>
<FormField.Input as={TextInput} name="last" />
</FormField>
<FormField grow>
<FormField.Label>Favorite Color</FormField.Label>
<FormField.Field>
<Select items={FAVORITE_COLOR_OPTIONS}>
<FormField.Input as={Select.Input} name="favoriteColor" />
<Select.Popper>
<Select.Card>
<Select.List>{item => <Select.Item>{item}</Select.Item>}</Select.List>
</Select.Card>
</Select.Popper>
</Select>
</FormField.Field>
</FormField>
</Modal.Body>
<Modal.ButtonGroup>
<Modal.CloseButton>Cancel</Modal.CloseButton>
<PrimaryButton type="submit">Submit</PrimaryButton>
</Modal.ButtonGroup>
</Modal.Card>
</Modal.Overlay>
</Modal>
);
};
Accessibility
Ensure users of assistive technology can discover, name, and operate a modal dialog: the rest of
the page is blocked by an overlay, background content is hidden from assistive technology via
sibling aria-hidden, keyboard focus is trapped inside the modal, the dialog has an accessible
name that matches its visible heading, and keyboard users can open and dismiss it predictably.
Use Modal when the user must complete or acknowledge a task before continuing with the page. For non-blocking tasks, use Dialog instead. Prefer Modal for the standard blocking dialog; use Popup with composed hooks when you need a custom popup stack or to omit behaviors (for example Escape or overlay dismiss). For portals, reading order, and related tradeoffs, see Guides > Accessibility > Inline Popups . See also the Modal Dialog Pattern | APG | WAI | W3C .
Minimum Accessible Structure
The following matches the Basic Example layout: Modal.CloseIcon before
Modal.Heading so open focus lands on the dismiss control first; primary actions use
Modal.CloseButton (which closes the modal on activate).
import {PrimaryButton} from '@workday/canvas-kit-react/button';
import {Modal} from '@workday/canvas-kit-react/modal';
<Modal>
<Modal.Target as={PrimaryButton}>Open</Modal.Target>
<Modal.Overlay>
<Modal.Card>
<Modal.CloseIcon aria-label="Close" />
<Modal.Heading>Title</Modal.Heading>
<Modal.Body>Content</Modal.Body>
<Modal.ButtonGroup>
<Modal.CloseButton>Cancel</Modal.CloseButton>
<Modal.CloseButton as={PrimaryButton}>Acknowledge</Modal.CloseButton>
</Modal.ButtonGroup>
</Modal.Card>
</Modal.Overlay>
</Modal>;Include a dismiss control: Modal.CloseButton with visible text (for example “Cancel” or
“Close”), and/or Modal.CloseIcon when the design uses an icon-only dismiss (requires
aria-label or Tooltip). Use Modal.CloseButton for actions that should also close
the modal (for example “Acknowledge”). Compose with Modal.Overlay → Modal.Card (or
Modal.OverflowOverlay when the entire overlay should scroll).
Built-in Behaviors
Canvas Kit applies these automatically via useModalModel and Modal subcomponents. Do not
duplicate them in consuming code.
Popup behaviors (composed on the default model):
useInitialFocus— moves focus into the modal when it opens (default: first focusable element in DOM order; optional override viainitialFocusRefon the model)useReturnFocus— returns focus toModal.Target(or configured return target) when it closesuseCloseOnOverlayClick— pointer interaction on the overlay (outside the dialog) closes the modaluseCloseOnEscape— Escape closes the modaluseFocusTrap— Tab / Shift+Tab cycle focus inside the modal (keyboard focus does not leave the dialog)useAssistiveHideSiblings— appliesaria-hiddento siblings of the modal stack while openuseDisableBodyScroll— prevents background page scroll while the modal is open
ARIA and DOM (applied by hooks/subcomponents):
Modal.Card:role="dialog",aria-labelledbyreferencing the headingid, andaria-modal="false"Modal.Heading:idwired toModal.Card’saria-labelledby; when there is no icon-only close button before the heading,useModalHeadingmay temporarily settabindex="0"on the heading so initial focus still lands near the start of the dialogModal.CloseIcon/Modal.CloseButton:onClickthat callsmodel.events.hide()Modal.Target:refandonClickto open and to receive return focus
Keyboard (trigger is Modal.Target, default SecondaryButton):
- Enter / Space on the trigger opens the modal (standard button behavior)
- On open and close, focus is managed by
useInitialFocusanduseReturnFocus(application overrides: see Focus management in Accessibility Requirements) - Tab / Shift+Tab move focus through interactive elements inside the modal; focus stays trapped within the dialog
- Escape closes the modal and returns focus per
useReturnFocus(unless Escape dismiss is omitted via a custom model—see Accept-only / no Escape dismiss)
Screen reader expectations (when built-in behaviors are used as intended):
- On open, assistive technology should announce the first focused control (often a dismiss control),
the dialog name (
Modal.Heading), anddialogrole - Sibling elements of the modal stack receive
aria-hidden="true"while the modal is visible, which hides the rest of the page from many assistive technologies - Trapping keyboard focus does not stop all screen reader virtual-cursor movement outside the dialog; treat the trap as the primary keyboard affordance, not a hard boundary—verify behavior in your supported browser and screen reader combinations
Accessibility Requirements
Required in application code for an accessible Modal. Hoist useModalModel when you need to
configure focus targets, open without Modal.Target, or control when the modal closes (for
example form validation). Rows marked (conditional) apply only when the situation
matches—otherwise omit.
If no design spec is provided: use default focus behavior; include a dismiss control and
Modal.Heading; omit initialFocusRef, returnFocusRef, and aria-describedby.
Do not remove Escape or overlay dismiss unless the design requires accept-only confirmation.
Focus management — defaults and developer prompts: 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 |
|---|---|---|
| Modal opens | useInitialFocus moves focus to the first focusable element in DOM order inside the modal (often Modal.CloseIcon or Modal.CloseButton). Omit initialFocusRef. | Which element should receive focus when the modal opens? (Only when the default first focusable element is wrong for the design.) Attach initialFocusRef to that element on useModalModel. |
| Modal closes | useReturnFocus moves focus to Modal.Target. Omit returnFocusRef. | Which element should receive focus when the modal closes? (Only when return focus should land somewhere other than Modal.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 Return Focus.
Custom targets (conditional): Apply when using a custom as component on
Modal.Target. Modal.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 modal can open
programmatically before the user clicks the target).
| Requirement | How to satisfy |
|---|---|
| Accessible dialog name | Use Modal.Heading so aria-labelledby on Modal.Card references a visible title. Do not omit the heading: Modal.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 modal: Modal.CloseButton with visible text (no extra aria-label needed), and/or Modal.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 Modal.Heading, assign a unique id to supplementary text and pass aria-describedby on Modal.Card. See Open focus below the heading below, Custom Focus, and Popup > Initial Focus (button-focus variant). |
| Keyboard-scrollable overflowing body (conditional) | When Modal.Body content overflows, set tabIndex={0} on Modal.Body so keyboard users can focus the scroll region and use arrow keys. See Body Content Overflow. |
| Accept-only / no Escape dismiss (conditional) | Only when the design requires the user to accept (not dismiss via Escape or overlay click): compose a custom usePopupModel with the modal behaviors you still need, omitting useCloseOnEscape and useCloseOnOverlayClick. See Without Close Icon. |
Open focus below the heading (conditional; see supplementary copy row above):
When open focus moves past the heading (for example into a form field), wire aria-describedby
so assistive technology still announces the supplementary copy. For focusing a primary action
instead of an input, see
Popup > Initial Focus .
import React from 'react';
import {useUniqueId} from '@workday/canvas-kit-react/common';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {Modal, useModalModel} from '@workday/canvas-kit-react/modal';
import {TextInput} from '@workday/canvas-kit-react/text-input';
const Example = () => {
const descriptionId = useUniqueId();
const inputRef = React.useRef<HTMLInputElement>(null);
const model = useModalModel({initialFocusRef: inputRef});
return (
<Modal model={model}>
<Modal.Target>Open</Modal.Target>
<Modal.Overlay>
<Modal.Card aria-describedby={descriptionId}>
<Modal.CloseIcon aria-label="Close" />
<Modal.Heading>Title</Modal.Heading>
<Modal.Body>
<p id={descriptionId}>Enter your email to continue.</p>
<FormField>
<FormField.Label>Email</FormField.Label>
<FormField.Input as={TextInput} ref={inputRef} />
</FormField>
</Modal.Body>
<Modal.CloseButton>Cancel</Modal.CloseButton>
</Modal.Card>
</Modal.Overlay>
</Modal>
);
};Summary for code generation:
- REQUIRED: accessible name, dismiss control, keyboard-operable trigger,
Modal.Overlay→Modal.Cardcomposition - CONDITIONAL:
initialFocusRef,returnFocusRef,aria-describedby,forwardRefon customModal.Target,tabIndex={0}on overflowingModal.Body, custom model omitting Escape/overlay dismiss,Modal.OverflowOverlay
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 dialogidonModal.CardorModal.Heading— Canvas Kit hooks wire these - Override
aria-modalto"true"onModal.Card— whenaria-modalistrue, some assistive technologies hide everything outside the dialog, including portaled UI owned by the modal (such as a Select menu rendered as a sibling). Canvas Kit setsaria-modal="false"for a better VoiceOver experience whileuseAssistiveHideSiblingsappliesaria-hiddento background siblings. Do not change this unless accessibility has approved it. Unlike Dialog, Modal also does not use the siblingaria-ownsreading-order pattern - Omit
Modal.Overlay(orModal.OverflowOverlay), renderModal.Cardoutside it, or add a custom portal/restructure instead ofModal→Modal.Overlay→Modal.Card - Use
open/onCloseprops onModal— Modal has no controlled visibility props; useuseModalModelandmodel.events.show()/model.events.hide() - Use Dialog when the task must block the rest of the page, or add
useFocusRedirect/aria-ownsexpecting Modal-like blocking behavior — Modal uses a focus trap and sibling hiding instead - Set
initialFocusReforreturnFocusRefby default — state the default focus behavior first and ask the developer before overriding (see Focus management in Accessibility Requirements) - Add
aria-expandedoraria-haspopuponModal.Target— those attributes apply to non-modal dialogs (see Dialog / Popup); Modal moves focus into the dialog on open and must not use this pattern - Use a custom
Modal.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 Return Focus) - Omit Escape and overlay dismiss without an explicit accept-only design requirement, or remove
Modal.CloseIconwithout providing another dismiss path (see Accept-only / no Escape dismiss) - Leave overflowing
Modal.Bodycontent without a keyboard path to scroll (see Keyboard-scrollable overflowing body) - Nest multiple
Modalinstances without deliberate initial focus and return-focus planning - Assume the focus trap alone fully hides outside content from every assistive technology — verify supported browser and screen reader combinations
Component API
Modal
This component is the container component and does not render any semantic elements. It provides
a React Context model for the Modal subcomponents. If you manually pass a model to all
subcomponents, this container component isn't needed. If you do not pass a model, the Modal
container component will build a default one using useModalModel. Modal is a composition of a
component and has a similar structure to Popup.
Props
Props extend from . If a model is passed, props from ModalModelConfig are ignored.
| Name | Type | Description | Default |
|---|---|---|---|
children | ReactNode | The contents of the Dialog. Can be | |
model | | Optional model to pass to the component. This will override the default model created for the component. This can be useful if you want to access to the state and events of the model, or if you have nested components of the same type and you need to override the model provided by React Context. | |
elemPropsHook | ( | Optional hook that receives the model and all props to be applied to the element. If you use this, it is your responsibility to return props, merging as appropriate. For example, returning an empty object will disable all elemProps hooks associated with this component. This allows finer control over a component without creating a new one. |
Modal.Overlay
The Modal.Overlay is the component that hooks a Modal up to the as well
as the semi-transparent overlay commonly used with modals. Internally, the Modal.Overlay
component uses two div elements to ensure proper rendering of the Modal content. The
default element is a div element and can be changed via the as prop.
Layout Component
Modal.Overlay supports all props from thelayout component.
Props
Props extend from div. Changing the as prop will change the element interface.
| Name | Type | Description | Default |
|---|---|---|---|
cs | | The | |
children | ReactNode | ||
as | React.ElementType | Optional override of the default element used by the component. Any valid tag or Component. If you provided a Component, this component should forward the ref using Note: Not all elements make sense and some elements may cause accessibility issues. Change this value with care. | div |
ref | React.Ref<R = div> | Optional ref. If the component represents an element, this ref will be a reference to the real DOM element of the component. If | |
model | | Optional model to pass to the component. This will override the default model created for the component. This can be useful if you want to access to the state and events of the model, or if you have nested components of the same type and you need to override the model provided by React Context. | |
elemPropsHook | ( | Optional hook that receives the model and all props to be applied to the element. If you use this, it is your responsibility to return props, merging as appropriate. For example, returning an empty object will disable all elemProps hooks associated with this component. This allows finer control over a component without creating a new one. |
Modal.Card
The Modal.Card is wraps a which wraps a . It is
the role="dialog" element and is uses useModalCard behavior hook which sets
aria-modal="false" and sets the aria-labelledby that points to the id of the
. If you don't use a Modal.Heading, add an aria-label
instead. The default element is a div and can be changed via the as prop.
Layout Component
Modal.Card supports all props from thelayout component.
Props
Props extend from div. Changing the as prop will change the element interface.
| Name | Type | Description | Default |
|---|---|---|---|
children | ReactNode | Children of the Card. Should contain a | |
variant | 'alt' | 'tonal' | The variant of the Card. Can be | 'default' |
cs | | The | |
as | React.ElementType | Optional override of the default element used by the component. Any valid tag or Component. If you provided a Component, this component should forward the ref using Note: Not all elements make sense and some elements may cause accessibility issues. Change this value with care. | div |
ref | React.Ref<R = div> | Optional ref. If the component represents an element, this ref will be a reference to the real DOM element of the component. If | |
model | | Optional model to pass to the component. This will override the default model created for the component. This can be useful if you want to access to the state and events of the model, or if you have nested components of the same type and you need to override the model provided by React Context. | |
elemPropsHook | ( | Optional hook that receives the model and all props to be applied to the element. If you use this, it is your responsibility to return props, merging as appropriate. For example, returning an empty object will disable all elemProps hooks associated with this component. This allows finer control over a component without creating a new one. |
Modal.Heading
The Modal.Heading semantically labels the Modal via adding an id that the
points to via aria-labelledby. If this component is not used,
you must add an aria-label to the Modal.Card to label the Modal for screen reader users.
This component uses the useModalHeading behavior hook which sets an id and also does some
focus management logic for situations where there is no
component used. Please use Modal.Heading and don't
use your own unless you also use the useModalHeading hook in your component. Consult
accessibility if you cannot use this component. The default element is an h2 and can be
changed via the as prop.
Layout Component
Modal.Heading supports all props from thelayout component.
Props
Props extend from h2. Changing the as prop will change the element interface.
| Name | Type | Description | Default |
|---|---|---|---|
children | ReactNode | ||
id | string | The id of the Card heading. Tie this to an | |
cs | | The | |
as | React.ElementType | Optional override of the default element used by the component. Any valid tag or Component. If you provided a Component, this component should forward the ref using Note: Not all elements make sense and some elements may cause accessibility issues. Change this value with care. | h2 |
ref | React.Ref<R = h2> | Optional ref. If the component represents an element, this ref will be a reference to the real DOM element of the component. If | |
model | | Optional model to pass to the component. This will override the default model created for the component. This can be useful if you want to access to the state and events of the model, or if you have nested components of the same type and you need to override the model provided by React Context. | |
elemPropsHook | ( | Optional hook that receives the model and all props to be applied to the element. If you use this, it is your responsibility to return props, merging as appropriate. For example, returning an empty object will disable all elemProps hooks associated with this component. This allows finer control over a component without creating a new one. |
useModalHeading
(
,
(
model: ,
elemProps: {},
ref: React.Ref
) => {
ref: (instance: | null) => void;
onBlur: () => void;
}
)Modal.Body
Layout Component
Modal.Body supports all props from thelayout component.
Props
Props extend from div. Changing the as prop will change the element interface.
| Name | Type | Description | Default |
|---|---|---|---|
cs | | The | |
children | ReactNode | ||
as | React.ElementType | Optional override of the default element used by the component. Any valid tag or Component. If you provided a Component, this component should forward the ref using Note: Not all elements make sense and some elements may cause accessibility issues. Change this value with care. | div |
ref | React.Ref<R = div> | Optional ref. If the component represents an element, this ref will be a reference to the real DOM element of the component. If | |
model | | Optional model to pass to the component. This will override the default model created for the component. This can be useful if you want to access to the state and events of the model, or if you have nested components of the same type and you need to override the model provided by React Context. | |
elemPropsHook | ( | Optional hook that receives the model and all props to be applied to the element. If you use this, it is your responsibility to return props, merging as appropriate. For example, returning an empty object will disable all elemProps hooks associated with this component. This allows finer control over a component without creating a new one. |
Modal.OverflowOverlay
If content is large, scrolling the entire overlay container is an option. Use the
Modal.OverflowOverlay component instead of the component. The 's maxHeight and height will need to be reset
to inherit to prevent any internal overflow.
This component should be used in place of the component if
full body overflow is desired.
Layout Component
Modal.OverflowOverlay supports all props from thelayout component.
Props
Props extend from div. Changing the as prop will change the element interface.
| Name | Type | Description | Default |
|---|---|---|---|
cs | | The | |
children | ReactNode | ||
as | React.ElementType | Optional override of the default element used by the component. Any valid tag or Component. If you provided a Component, this component should forward the ref using Note: Not all elements make sense and some elements may cause accessibility issues. Change this value with care. | div |
ref | React.Ref<R = div> | Optional ref. If the component represents an element, this ref will be a reference to the real DOM element of the component. If | |
model | | Optional model to pass to the component. This will override the default model created for the component. This can be useful if you want to access to the state and events of the model, or if you have nested components of the same type and you need to override the model provided by React Context. | |
elemPropsHook | ( | Optional hook that receives the model and all props to be applied to the element. If you use this, it is your responsibility to return props, merging as appropriate. For example, returning an empty object will disable all elemProps hooks associated with this component. This allows finer control over a component without creating a new one. |
Model
useModalModel
This model hook uses and pre-configures behaviors that are required for an
accessible modal. useModalModel should be used in most cases, but if you require custom
behaviors, you can use usePopupModel directly. Be sure to add proper popup behaviors to ensure
the modal is accessible.
The following behaviors are added to the PopupModel:
You can pass the Modal model config either directly to the Modal component or to the
useModalModel hook, but not both. A model prop always takes precedence over the config passed
to the useModalModel hook. If no model is passed to a Modal component, a ModalModel will
be created for you. Creating your own model hoists the modal's state to the level of your
component and allows you to access the model's state and events.
const model = useModalModel(config);
<Modal model={model}>
// ...
</Modal>
useModalModel (config: ):