Usage
import { Modal } from '@loomhq/lens'
<Modal title="Title">Content</Modal>
Accessibility
Please verify that your component supports the following accessibility features that come out of box and none of your modifications negate these features.
Modal
should:
- Be focusable and togglable (open and closeable) by every input device (mouse, keyboard, switch, etc).
- Have a clear trigger to open: Focusable element (i.e.,
<Button type="button" ...>
)
- Have a clear trigger to close: Natively, we provide the
X
button. If you create your own, please ensure your close trigger supports the below features.
- Prevent the body from scrolling in the background when it is open.
Keyboard navigability
When the Modal
(dialog
HTML element) is open
, these are the expected behaviours:
-
Autofocus: The focus should shift from the trigger that opened the Modal
to the first focusable element (usually the close button
).
-
Tab: The user should be able to Tab
to the next focusable element, and Shift + Tab
to the previous focusable element within the Modal
.
-
Close Modal: The user should be able to close the modal with Esc
-
Trap Focus: The user should not be able to focus outside of the Modal
. The last focusable item should loop back around to the first focusable item.
-
Return Focus: When the Modal
is closed, the focus should return to the trigger that opened the Modal
(usually a button).
Screenreader support
Screenreader support is still [WIP]. In-code comments outlines the remaining work with Linear ticket references.
Modal with all elements
() => {
const [isOpen, setIsOpen] = React.useState(false)
return (
<>
<Button onClick={() => setIsOpen(!isOpen)} variant="primary">
Open Modal
</Button>
<Modal
mainButton={<Button variant="primary">Confirm</Button>}
secondaryButton={<Button>Cancel</Button>}
alternativeButton={<TextButton icon={<SvgAdd />}>Add</TextButton>}
title={demoText.short}
isOpen={isOpen}
onCloseClick={() => setIsOpen(!isOpen)}
>
{demoText.medium}
</Modal>
</>
)}
With dividers
() => {
const [isOpen, setIsOpen] = React.useState(false)
return (
<>
<Button onClick={() => setIsOpen(!isOpen)} variant="primary">
Open Modal
</Button>
<Modal
hasDividers
mainButton={<Button variant="primary">Confirm</Button>}
secondaryButton={<Button>Cancel</Button>}
alternativeButton={<TextButton icon={<SvgAdd />}>Add</TextButton>}
title={demoText.short}
isOpen={isOpen}
onCloseClick={() => setIsOpen(!isOpen)}
>
{demoText.alphabet.map((letter, index) => (
<Container
paddingTop="small"
paddingBottom="small"
borderSide="bottom"
key={index}
>{letter}</Container>
))}
</Modal>
</>
)}
With custom max-height
() => {
const [isOpen, setIsOpen] = React.useState(false)
return (
<>
<Button onClick={() => setIsOpen(!isOpen)} variant="primary">
Open Modal
</Button>
<Modal
maxHeight="80vh"
hasDividers
mainButton={<Button variant="primary">Confirm</Button>}
secondaryButton={<Button>Cancel</Button>}
alternativeButton={<TextButton icon={<SvgAdd />}>Add</TextButton>}
title={demoText.short}
isOpen={isOpen}
onCloseClick={() => setIsOpen(!isOpen)}
>
{demoText.alphabet.map((letter, index) => (
<Container
paddingTop="small"
paddingBottom="small"
borderSide="bottom"
key={index}
>{letter}</Container>
))}
</Modal>
</>
)}
Bottom alignment
Best for mobile modals. Recommend setting a max width for your internal components.
() => {
const [isOpen, setIsOpen] = React.useState(false)
return (
<>
<Button onClick={() => setIsOpen(!isOpen)} variant="primary">
Open Modal
</Button>
<Modal
placement="bottom"
maxWidth="unset"
isOpen={isOpen}
onCloseClick={() => setIsOpen(!isOpen)}
>
<Container maxWidth={60} width="100%" marginX="auto">
{demoText.alphabet.map((letter, index) => (
<Container
paddingTop="small"
paddingBottom="small"
borderSide="bottom"
key={index}
>{letter}</Container>
))}
</Container>
</Modal>
</>
)}
Custom Modal with Bottom Drawer
With scrollable section
() => {
const [isOpen, setIsOpen] = React.useState()
return (
<>
<Button onClick={() => setIsOpen(!isOpen)} variant="primary">
Open Modal
</Button>
<Backdrop
isOpen={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
<ModalCard
onCloseClick={() => setIsOpen(!isOpen)}
isOpen={isOpen}
maxWidth={72}
>
<Arrange rows={['1fr', 'auto']}>
<Container
overflow="auto"
maxHeight="100%"
padding="xlarge"
>
<Text size="large">
{demoText.veryLong}
</Text>
</Container>
<Container backgroundColor="highlight" padding="xlarge">
{demoText.medium}
</Container>
</Arrange>
</ModalCard>
</Backdrop>
</>
)}
We should avoid this pattern and only use it when the modal is a MANDATORY, blocking flow. This should not be used for aesthetic reasons. In general, almost all modals should have an X.
If you use this pattern, you MUST provide a way to progress or close the modal in some way, even if your API calls fail or other errors occur.
no close x
() => {
const [isOpen, setIsOpen] = React.useState()
const [showLoader, setShowLoader] = React.useState(true)
React.useEffect(() => {
if (isOpen) {
setTimeout(() => {
setShowLoader(false);
}, 2000)
}
}, [isOpen])
return (
<>
<Button onClick={() => setIsOpen(!isOpen)} variant="primary">
Open Modal
</Button>
<Backdrop
isOpen={isOpen}
>
<ModalCard
onCloseClick={() => setIsOpen(!isOpen)}
isOpen={isOpen}
maxWidth={72}
removeClose={true}
>
<Container
overflow="auto"
maxHeight="100%"
padding="xlarge"
>
<Text size="large">
My children do not have any tabbable nodes, yet I am able to render correctly. Use me sparingly, and only for cases where the user <i>must</i> perform an action.
</Text>
<Spacer top="medium" />
<Text size="large">
Always provide a way for the user to continue (The close button will show after 2s, imitating an api call wait in a component).
</Text>
</Container>
<Container backgroundColor="highlight" padding="xlarge">
{showLoader && <Loader />}
{!showLoader && <Button variant="primary" hasFullWidth onClick={() => setIsOpen(false)}>Close</Button>}
</Container>
</ModalCard>
</Backdrop>
</>
)}
Props
Modal
maxHeight
number | string
'70vh'
maxWidth
number | string
60
placement
'center' | 'bottom'
'center'
mainButton
React.ReactNode
secondaryButton
React.ReactNode
alternativeButton
React.ReactNode
onCloseClick
React.ReactEventHandler
onBackgroundClick
React.ReactEventHandler
onKeyDown
React.ReactEventHandler
ModalCard
maxWidth
number | string
60
maxHeight
number | string
'70vh'
onKeyDown
React.ReactEventHandler
onCloseClick
React.ReactEventHandler