Skip to main content

Drawer

The Drawer component is a modal component that can be positioned to the left, right, top or bottom of the viewport. It can be used to display additional information or actions without leaving the current screen. It renders contents similar to the Modal component and supports a header, body, fixed footer and icon.

Result
Loading...
Live Editor
function Example() {
	const [show, setShow] = useState(false);

	const handleClose = () => setShow(false);
	const handleShow = () => setShow(true);

	return (
		<div data-testid="pw-drawer">
			<Button variant="primary" onClick={handleShow}>
				Toggle
			</Button>

			<Drawer show={show} onHide={handleClose}>
				<Drawer.Header
					closeButton
					renderIcon={() => (
						<Drawer.Icon>
							<span className="icon-message-circle-02" />
						</Drawer.Icon>
					)}
					title="Title"
					subtitle="Subtitle"
				/>
				<Drawer.Body>
					Some text as placeholder. In real life you can have the elements you have chosen. Like,
					text, images, lists, etc.
				</Drawer.Body>
				<Drawer.Footer>
					<Button onClick={handleClose} variant="outline-secondary">
						Cancel
					</Button>
					<Button onClick={handleClose}>Save</Button>
				</Drawer.Footer>
			</Drawer>
		</div>
	);
}

Variations, Scrolling, and Tooltips

The Drawer components allow for customization similar to Modal. The body supports the prop bordered which will render a variant of the body that places a border at the top and bottom of the body.

This example also shows using the scrollable prop on the body to allow for scrolling within the body.

❗Tooltips need to be attached to a container outside of the Drawer.Body. Here the Tooltip on the Select label is attached to the Drawer.Header.

Result
Loading...
Live Editor
function Example() {
	const [show, setShow] = useState(false);
	const [isSlidden, setIsSlidden] = useState(false);
	const drawerHeaderRef = useRef(null);

	const handleClose = () => setShow(false);
	const handleShow = () => setShow(true);

	return (
		<div data-testid="pw-drawer-scrollable">
			<Button variant="primary" onClick={handleShow}>
				Toggle
			</Button>

			<Drawer show={show} onHide={handleClose} placement="end">
				<Drawer.Header
					closeButton
					renderIcon={() => (
						<Drawer.Icon variant="danger">
							<span className="icon-placeholder" />
						</Drawer.Icon>
					)}
					title="Title"
					ref={drawerHeaderRef}
				/>
				<Drawer.Body bordered scrollable>
					<Form footerRenderer={() => null}>
						<FormField>
							<Select
								name="select"
								label={
									<div className="d-flex">
										<span>Label</span>
										<Tooltip title="I am a tooltip" container={() => drawerHeaderRef.current}>
											<span className="ms-3 icon-help-circle" />
										</Tooltip>
									</div>
								}
							>
								<Item key="one">One</Item>
								<Item key="two">Two</Item>
								<Item key="three">Three</Item>
							</Select>
						</FormField>

						{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map((i) => (
							<FormField key={i}>
								<TextField
									name={`text${i}`}
									label={
										<Label
											renderAction={() => (
												<Button variant="link" className="p-0" onClick={() => setIsSlidden(true)}>
													Show more
												</Button>
											)}
										>
											Field {i}
										</Label>
									}
								/>
							</FormField>
						))}
					</Form>

					<Drawer.Slide show={isSlidden} onClickBack={() => setIsSlidden(false)}>
						<p>More stuff related to Field 1</p>
						<p>This could be additional fields, supplemental info, etc.</p>

						{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map((i) => (
							<TextField key={`slide-${i}`} name={`text${i}`} label="Text Field" />
						))}

						<p>This could be additional fields, supplemental info, etc.</p>
					</Drawer.Slide>
				</Drawer.Body>
				<Drawer.Footer>
					<Button onClick={handleClose} variant="outline-secondary">
						Cancel
					</Button>
					<Button onClick={handleClose}>Save</Button>
				</Drawer.Footer>
			</Drawer>
		</div>
	);
}

Sizes

The size of the Drawer can be customized. However, all Drawers will be full screen below the medium breakpoint.

Result
Loading...
Live Editor
function Example({ size }) {
	const [show, setShow] = useState(false);

	const handleClose = () => setShow(false);
	const handleShow = () => setShow(true);

	return (
		<>
			<Button variant="primary" onClick={handleShow} className="mb-2">
				Toggle {size}
			</Button>

			<Drawer show={show} onHide={handleClose} size={size}>
				<Drawer.Header title={size} />
				<Drawer.Body bordered>Body</Drawer.Body>
				<Drawer.Footer>
					<Button onClick={handleClose} variant="outline-secondary">
						Cancel
					</Button>
					<Button onClick={handleClose}>Save</Button>
				</Drawer.Footer>
			</Drawer>
		</>
	);
}

render(['sm', 'md', 'lg', 'xl'].map((size) => <Example size={size} />));

Responsive Footers

Responsive footers will adjust the display order of their button children below the small breakpoint.

Result
Loading...
Live Editor
function Example() {
	const [show, setShow] = useState(false);

	const handleClose = () => setShow(false);
	const handleShow = () => setShow(true);

	return (
		<>
			<Button variant="primary" onClick={handleShow} className="mb-2">
				Toggle
			</Button>

			<Drawer show={show} onHide={handleClose}>
				<Drawer.Header title="Reponsive Footer" />
				<Drawer.Body bordered>Body</Drawer.Body>
				<Drawer.Footer responsive>
					<Button onClick={handleClose} variant="outline-secondary">
						Cancel
					</Button>
					<Button onClick={handleClose}>Save</Button>
				</Drawer.Footer>
			</Drawer>
		</>
	);
}

With DrawerSlide

The DrawerSlide component lets you display additional content in a Drawer. When shown, it slides in and covers the body of your Drawer, and the body content it covers is made inert while it's open. The header stays put, so the close button remains visible and keyboard reachable.

An open slide hides the Drawer's footer, on the assumption that a slide's own actions live inside it. If instead your footer holds the primary action for each step of a slide flow, pass persistent to DrawerFooter to keep it visible and keyboard reachable as slides change.

Result
Loading...
Live Editor
function Example() {
	const [show, setShow] = useState(false);
	const [isSlidden, setIsSlidden] = useState(false);
	const [isSliddenToo, setIsSliddenToo] = useState(false);

	return (
		<div data-testid="pw-drawer-slide">
			<Button onClick={() => setShow(true)}>Toggle Drawer</Button>

			<Drawer show={show} onHide={() => setShow(false)} placement="end">
				<Drawer.Header
					closeButton
					renderIcon={() => (
						<Drawer.Icon>
							<span className="icon-placeholder" />
						</Drawer.Icon>
					)}
					title="Title"
					subtitle="Subtitle"
				/>
				<Drawer.Body bordered>
					<Form footerRenderer={() => null}>
						<FormField>
							<TextField
								name="text"
								label={
									<Label
										renderAction={() => (
											<Button variant="link" className="p-0" onClick={() => setIsSlidden(true)}>
												Show more
											</Button>
										)}
									>
										Field 1
									</Label>
								}
							/>
						</FormField>

						<FormField>
							<TextField
								name="text"
								label={
									<Label
										renderAction={() => (
											<Button variant="link" className="p-0" onClick={() => setIsSliddenToo(true)}>
												Show more
											</Button>
										)}
									>
										Field 2
									</Label>
								}
							/>
						</FormField>
					</Form>

					<Drawer.Slide show={isSlidden} onClickBack={() => setIsSlidden(false)}>
						<p>More stuff related to Field 1</p>
						<p>This could be additional fields, supplemental info, etc.</p>
					</Drawer.Slide>

					<Drawer.Slide show={isSliddenToo} onClickBack={() => setIsSliddenToo(false)}>
						<p>More stuff related to Field 2</p>
						<p>This could be additional fields, supplemental info, etc.</p>
					</Drawer.Slide>
				</Drawer.Body>
				<Drawer.Footer>
					<Button variant="outline-secondary" onClick={() => setShow(false)}>
						Cancel
					</Button>
					<Button>Submit</Button>
				</Drawer.Footer>
			</Drawer>
		</div>
	);
}

Slides can be stacked to build a multi-step flow. Keep each step's slide shown once the flow has reached it, so the steps layer over one another and going back reveals the previous one — the slide that renders last sits on top.

When each step's primary action lives in the footer rather than in the slide, pass persistent to DrawerFooter so it stays visible and keyboard reachable as the steps change. Swap its contents per step. The header's close button stays reachable throughout, and the body content underneath the slides is inert while any of them is open.

Result
Loading...
Live Editor
function Example() {
	const STEPS = ['options', 'selectUsers', 'confirm', 'success'];

	const [show, setShow] = useState(false);
	const [step, setStep] = useState('options');
	const [selected, setSelected] = useState([]);
	const [authorized, setAuthorized] = useState(false);

	const reached = (name) => STEPS.indexOf(step) >= STEPS.indexOf(name);

	const price = 800;
	const total = selected.length * price;

	const users = ['Ada Lovelace', 'Grace Hopper', 'Katherine Johnson'];

	const toggleUser = (name, isSelected) =>
		setSelected((current) =>
			isSelected ? [...current, name] : current.filter((user) => user !== name),
		);

	const reset = () => {
		setStep('options');
		setSelected([]);
		setAuthorized(false);
		setShow(false);
	};

	return (
		<div>
			<Button onClick={() => setShow(true)}>Add Premium Portal</Button>

			<Drawer show={show} onHide={reset} placement="end" size="md">
				<Drawer.Header
					closeButton
					title="Add Premium Client Portal"
					renderIcon={() => (
						<Drawer.Icon>
							<span className="icon-user-plus-01" />
						</Drawer.Icon>
					)}
				/>

				<Drawer.Body bordered>
					<div className="h4 k-mb-4">2 ways to get Premium Client Portal</div>
					<Card size="sm" border shadow="none">
						<Card.Body>
							<div className="d-flex justify-content-between align-items-center k-mb-2">
								<div className="fs-lg fw-semibold">Purchase an Add-on</div>
								<div className="text-gray-600">${price}/yr per advisor</div>
							</div>
							<p className="k-mb-2-5 text-gray-800">
								Add Premium Client Portal to your existing subscriptions for an additional fee.
							</p>
							<Button variant="outline-secondary" onClick={() => setStep('selectUsers')}>
								Purchase Add-on
							</Button>
						</Card.Body>
					</Card>

					<Drawer.Slide
						show={reached('selectUsers')}
						onClickBack={() => {
							setSelected([]);
							setStep('options');
						}}
					>
						<div className="h4 k-mb-3">Select Users to Upgrade</div>
						{users.map((user) => (
							<div key={user} className="k-mb-2">
								<Checkbox
									label={user}
									isSelected={selected.includes(user)}
									onChange={(isSelected) => toggleUser(user, isSelected)}
								/>
							</div>
						))}
					</Drawer.Slide>

					<Drawer.Slide
						show={reached('confirm')}
						onClickBack={() => setStep('selectUsers')}
						renderBackButton={() => (
							<Button
								variant="link"
								className="text-decoration-none p-0 mb-3 focus-ring"
								onClick={() => setStep('selectUsers')}
							>
								<span className="icon-chevron-left me-2" />
								Back to User Selection
							</Button>
						)}
					>
						<div className="h4 k-mb-3">Confirm Your Order</div>
						<div className="border rounded k-p-3 k-mb-3">
							<div className="d-flex justify-content-between k-mb-2">
								<span className="text-gray-800">Add-ons</span>
								<span className="text-gray-800">{selected.length}</span>
							</div>
							<div className="d-flex justify-content-between">
								<span className="h5 k-mb-0">Total</span>
								<span className="h5 k-mb-0">${total}</span>
							</div>
						</div>
						<Checkbox
							isSelected={authorized}
							onChange={setAuthorized}
							label="I am authorized to make changes to this firm's subscription."
						/>
					</Drawer.Slide>

					<Drawer.Slide show={reached('success')} renderBackButton={() => null}>
						<div className="h2 text-center k-mb-2-5">You&rsquo;re all set!</div>
						<p className="fs-lg text-center text-gray-800">
							{selected.length} {selected.length === 1 ? 'user' : 'users'} will have Premium Client
							Portal enabled soon.
						</p>
					</Drawer.Slide>
				</Drawer.Body>

				<Drawer.Footer persistent className="d-flex justify-content-between align-items-center">
					{step === 'selectUsers' && (
						<>
							<span className="text-gray-600">{selected.length > 0 && `Subtotal: $${total}`}</span>
							<Button disabled={selected.length === 0} onClick={() => setStep('confirm')}>
								Confirm {selected.length} Add-{selected.length === 1 ? 'on' : 'ons'}
							</Button>
						</>
					)}

					{step === 'confirm' && (
						<Button disabled={!authorized} onClick={() => setStep('success')}>
							Confirm Order
						</Button>
					)}

					{step === 'success' && <Button onClick={reset}>Close</Button>}

					{step === 'options' && <span className="text-gray-600">Choose an option to continue</span>}
				</Drawer.Footer>
			</Drawer>
		</div>
	);
}

Using Tabs

Result
Loading...
Live Editor
function Example() {
	const [show, setShow] = useState(false);
	const [activeTabId, setActiveTabId] = useState('Ownership');

	const handleClose = () => setShow(false);
	const handleShow = () => setShow(true);

	const tabsList = [
		<Tab key="Ownership" id="Ownership">
			Ownership
		</Tab>,
		<Tab key="Fees" id="Fees">
			Fees
		</Tab>,
		<Tab key="Realization" id="Realization">
			Realization
		</Tab>,
		<Tab key="Holdings" id="Holdings">
			Holdings
		</Tab>,
	];

	const tabContents = useMemo(() => {
		switch (activeTabId) {
			case 'Ownership':
				return 'Ownership Content';
			case 'Fees':
				return 'Fees Content';
			case 'Realization':
				return 'Realization Content';
			case 'Holdings':
				return 'Holdings Content';
			default:
				return null;
		}
	}, [activeTabId]);

	return (
		<>
			<Button variant="primary" onClick={handleShow}>
				Toggle
			</Button>

			<Drawer show={show} onHide={handleClose}>
				<Drawer.Header
					closeButton
					renderIcon={() => (
						<Drawer.Icon>
							<span className="icon-message-circle-02" />
						</Drawer.Icon>
					)}
					title="Title"
					subtitle="Subtitle"
					className="pb-0"
				/>
				<Drawer.Body className="px-0">
					<Tabs
						variant="tabs"
						tabs={tabsList}
						activeTabId={activeTabId}
						onTabClick={(event, id) => {
							setActiveTabId(id);
						}}
						className="k-px-3"
					></Tabs>
					<div className="border-top k-p-3" style={{ marginTop: 1 }}>
						{tabContents}
					</div>
				</Drawer.Body>
				<Drawer.Footer>
					<Button onClick={handleClose} variant="outline-secondary">
						Cancel
					</Button>
					<Button onClick={handleClose}>Save</Button>
				</Drawer.Footer>
			</Drawer>
		</>
	);
}

Props

Drawer

DrawerHeader

The DrawerHeader supports a static set of props to render title, subtitle, and icon in a consistent manner. It also supports rendering children for more customization.

DrawerBody

DrawerFooter

The DrawerFooter component will automatically adjust the display of the buttons within such that they stack.

DrawerSlide

The DrawerSlide component allows you to slide in secondary content in a Drawer.