Control interaction flow with simple, extensible drawers.

Drawers are used occasionally in our application, when the user is required to make choices before they can proceed. Here are some examples:

  • Editing user settings: Allowing users to update profile details or notification preferences without navigating away from the main dashboard.

  • Viewing detailed logs: Displaying event logs, email histories, or system activity in a collapsible side panel for quick reference.

  • Initiating workflows: Providing a structured step-by-step interface for setting up automations, integrations, or campaign sequences.

A Drawer consists of several components:

  • Drawer
    • this component renders an overlay, and the drawer dialog itself.
  • DrawerHeader
    • contains the drawer's title and close button. Renders automatically from the title prop.
  • DrawerTitle
    • the title of the drawer, which renders inside the header.
  • DrawerBody
    • the main content of the drawer. If the drawer overflows, this is the part that will scroll by default.
  • DrawerFooter
    • container for the drawer's footer, which typically contains buttons.
  • DrawerInset
    • an optional component that may be used to render content all the way to the drawer's edges (over the padding).

Importing

The components can be imported via:

import {
	Drawer,
	DrawerBody,
	DrawerFooter,
	DrawerInset,
} from '@customerio/pluma-components/react';

Usage

At it's simplest, a Drawer will require:

  • isOpen prop
    • controls whether the drawer is rendered or not.
  • onClose prop
    • a function that will be called when the drawer requests to be closed - for example, when the close button is clicked. In this callback, you'll need to set your isOpen state accordingly.
  • title prop
    • a text string to render as the title of the drawer.
  • content, wrapped in DrawerBody
    • DrawerBody is required for the body to handle overflow scrolling correctly.
  • a DrawerFooter, with buttons or other actions
    • if there are no actions, the footer can be omitted.
import { Button, Paragraph, Drawer, DrawerBody, DrawerFooter } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [isOpen, setIsOpen] = useState(false);

	return <>
		<Button
			variant="primary"
			onClick={() => setIsOpen(true)}
		>
			Open Drawer
		</Button>

		<Drawer 
			isOpen={isOpen} 
			onClose={() => setIsOpen(false)}
			title="Example Drawer"
		>
			<DrawerBody>
				<Paragraph>
					This is the body of the drawer.
				</Paragraph>
			</DrawerBody>

			<DrawerFooter>
				<Button 
					onClick={() => setIsOpen(false)}
				>
					Close
				</Button>
			</DrawerFooter>
		</Drawer>
	</>
}

Open/close callbacks

The drawer component accepts a few callbacks related to its open state:

  • onClose - this is called when the drawer should be closed (for example, when the "close" "X" button is clicked)
    • the developer should use this callback to set the isOpen state to false - otherwise, the drawer will stay open
  • onOpen - this is called when the drawer first renders into the DOM
    • because the isOpen state is controlled from outside the drawer, this callback wouldn't be used to set isOpen. Instead, this can be used to react to a state change from closed to opened if isOpen is controlled by another parent component in the app
  • onCloseComplete - when a drawer closes, it will animate out, and unmount after the animation. This is called when the drawer fully unmounts, after the animation is finished

For example, open devtools and see the console logs on the below drawer:

import { Button, Paragraph, Drawer, DrawerBody, DrawerFooter } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [isOpen, setIsOpen] = useState(false);

	return <>
		<Button
			variant="primary"
			onClick={() => setIsOpen(true)}
		>
			Open Drawer
		</Button>

		<Drawer 
			isOpen={isOpen} 
			onOpen={() => {
				console.log('onOpen');
			}}
			onClose={() => {
				console.log('onClose');
				setIsOpen(false);
			}}
			onCloseComplete={() => {
				console.log('onCloseComplete');
			}}
			title="Example Drawer"
			animationTransitionDuration={500}
		>
			<DrawerBody>
				<Paragraph>
					This is the body of the drawer.
				</Paragraph>
			</DrawerBody>
		</Drawer>
	</>
}

Dismissable drawers

By default, drawers are dismissable and will close when:

  • the "close" button is clicked
  • the overlay is clicked
  • the "Escape" key is pressed

Depending on the use case, it may be necessary to disable one or more of these behaviors. Each of the dismissals can be controlled via a prop:

  • shouldShowCloseButton
    • if false, the close button in the header won't be rendered.
  • shouldCloseOnOverlayClick
    • if false, clicking the overlay won't close the drawer.
  • shouldCloseOnEscapePress
    • if false, pressing the "Escape" key won't close the drawer.

Additionally, there is an isDismissable prop, which is a shortcut for disabling all of the above.

Note: The individual props, if set, will still take precedence over the isDismissable setting.

import { Button, Paragraph, Drawer, DrawerBody, DrawerFooter } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [isOpen, setIsOpen] = useState(false);

	return <>
		<Button
			variant="primary"
			onClick={() => setIsOpen(true)}
		>
			Open Drawer
		</Button>

		<Drawer 
			isOpen={isOpen} 
			title="Can't dismiss"
			isDismissable={false}
		>
			<DrawerBody>
				<Paragraph>
					I can only be dismissed with the button below.
				</Paragraph>
			</DrawerBody>

			<DrawerFooter>
				<Button 
					onClick={() => setIsOpen(false)}
				>
					Close
				</Button>
			</DrawerFooter>
		</Drawer>
	</>
}

Placement

By default, the drawer will be on the right side of the screen. If you need to change the placement, you can use the placement prop.

import { Button, Paragraph, Drawer, DrawerBody, DrawerFooter } from '@customerio/pluma-components/react';

export default function Example() {
	const [isOpen, setIsOpen] = useState(false);

	return <>
		<Button
			variant="primary"
			onClick={() => setIsOpen(true)}
		>
			Open Drawer
		</Button>

		<Drawer 
			isOpen={isOpen} 
			onClose={() => setIsOpen(false)}
			title="Left Drawer"
			placement="left"
		>
			<DrawerBody>
				<Paragraph>
					I'm on the left side.
				</Paragraph>
			</DrawerBody>

			<DrawerFooter>
				<Button 
					onClick={() => setIsOpen(false)}
				>
					Close
				</Button>
			</DrawerFooter>
		</Drawer>
	</>
}

Size

If you have a small amount of content, you can use the size prop to make the drawer smaller.

import { Button, Paragraph, Drawer, DrawerBody, DrawerFooter } from '@customerio/pluma-components/react';

export default function Example() {
	const [isOpen, setIsOpen] = useState(false);

	return <>
		<Button
			variant="primary"
			onClick={() => setIsOpen(true)}
		>
			Open Drawer
		</Button>

		<Drawer 
			isOpen={isOpen} 
			onClose={() => setIsOpen(false)}
			title="Small Drawer"
			size="sm"
		>
			<DrawerBody>
				<Paragraph>
					I'm a small drawer.
				</Paragraph>
			</DrawerBody>

			<DrawerFooter>
				<Button 
					onClick={() => setIsOpen(false)}
				>
					Close
				</Button>
			</DrawerFooter>
		</Drawer>
	</>
}

Overlays

By default, drawers render an overlay. You can disable this by setting the withOverlay prop to false.

import { Button, Paragraph, Drawer, DrawerBody, DrawerFooter } from '@customerio/pluma-components/react';

export default function Example() {
	const [isOpen, setIsOpen] = useState(false);

	return <>
		<Button
			variant="primary"
			onClick={() => setIsOpen(true)}
		>
			Open Drawer
		</Button>

		<Drawer 
			isOpen={isOpen} 
			onClose={() => setIsOpen(false)}
			title="No Overlay"
			withOverlay={false}
		>
			<DrawerBody>
				<Paragraph>
					I'm a drawer with no overlay.
				</Paragraph>
			</DrawerBody>

			<DrawerFooter>
				<Button 
					onClick={() => setIsOpen(false)}
				>
					Close
				</Button>
			</DrawerFooter>
		</Drawer>
	</>
}

Custom content

By default DrawerBody contains padding to keep the content from the edges of the drawer. In some cases, it may be necessary for the content to stretch all the way to the edges - for example, when showing a full-width image or table.

To make this possible, the DrawerInset component can be used:

import { Button, Paragraph, Drawer, DrawerBody, DrawerFooter } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [isOpen, setIsOpen] = useState(false);

	return <>
		<Button
			variant="primary"
			onClick={() => setIsOpen(true)}
		>
			Open Drawer
		</Button>

		<Drawer 
			isOpen={isOpen} 
			onClose={() => setIsOpen(false)}
			title="Stretchy content"
		>
			<DrawerBody>
				<DrawerInset backgroundColor="accent-minimal" p="100">
					<Paragraph>I stretch all the way to the edges</Paragraph>
				</DrawerInset>

				<Box backgroundColor="information-minimal" p="100">
					<Paragraph>I don't stretch</Paragraph>
				</Box>
			</DrawerBody>

			<DrawerFooter>
				<Button 
					onClick={() => setIsOpen(false)}
				>
					Close
				</Button>
			</DrawerFooter>
		</Drawer>
	</>
}
import { Button, Paragraph, Drawer, DrawerBody, DrawerFooter } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [isOpen, setIsOpen] = useState(false);

	return <>
		<Button
			variant="primary"
			onClick={() => setIsOpen(true)}
		>
			Open Table Drawer
		</Button>

		<Drawer 
			isOpen={isOpen} 
			onClose={() => setIsOpen(false)}
			title="Stretchy content"
		>
			<DrawerBody>
				<DrawerInset>
					<Table caption="Australian birds">
						<TableThead>
							<TableTr>
								<TableTh>Name</TableTh>
								<TableTh>Genus</TableTh>
								<TableTh>Species</TableTh>
							</TableTr>
						</TableThead>

						<TableTbody>
							<TableTr>
								<TableTd>Australian Magpie</TableTd>
								<TableTd>Gymnorhina</TableTd>
								<TableTd>tibicen</TableTd>
							</TableTr>

							<TableTr>
								<TableTd>Galah</TableTd>
								<TableTd>Eolophus</TableTd>
								<TableTd>roseicapilla</TableTd>
							</TableTr>

							<TableTr>
								<TableTd>Rainbow lorikeet</TableTd>
								<TableTd>Trichoglossus</TableTd>
								<TableTd>moluccanus</TableTd>
							</TableTr>
						</TableTbody>
					</Table>
				</DrawerInset>
			</DrawerBody>

			<DrawerFooter>
				<Button 
					onClick={() => setIsOpen(false)}
				>
					Close
				</Button>
			</DrawerFooter>
		</Drawer>
	</>
}

Accessibility

To make drawer's accessible, the component contains the following features:

  • When a drawer is opened, focus moves to the first focusable element
  • When the drawer is closed, focus returns to the element that opened it
  • Drawers include a focus trap - tabbing through the drawer's elements will restrict focus to its contents only.

API

Default:125

The duration (in ms) for the modal animation.

By default, the modal will focus the first focusable element. You can override this by setting the index of the focusable element you want to focus instead.

A shortcut to set shouldShowCloseButton, shouldCloseOnOverlayClick, and shouldCloseOnEscapePress to false. The other prop values still take precedence, if set, even when isDismissable is true.

Whether the modal is open.

Called when the modal is requesting to be closed. For example, when clicking the close button, clicking the overlay, or pressing the Escape key.

Called when the modal finishes closing (after the close animation finishes).

Called when the modal is opened.

The placement of the drawer.

Whether onClose should be called when the Escape key is pressed.

Whether onClose should be called when the overlay is clicked.

Whether a close button should be displayed in the drawer.

A special prop containing all the other modal primitive props for use with the ModalsManager.

An optional subtitle displayed below the title. Only displayed when title is also provided.

The title of the drawer.

Whether or not the drawer should have an overlay.