Balance Logo
Balance
Reckon Design System
Open playroom

Side Drawer

Side drawer is used to overlay content on top of the interface. They are intended to capture the user's attention in order to inform or shift focus to a pertinent task.
Install
pnpm add @balance-web/side-drawer
Import usage
import {
SideDrawer,
SideDrawerProvider,
SideDrawerController
} from '@balance-web/side-drawer';
  • Code
  • API

Alternatives

  • Bottom drawer — Used for heavy forms, especially if they contain datatables, to support the user's main activity without disrupting it.
  • Product menu — Used to display a collapsable menu as an overlay.

Requirements

To handle the stacking behaviour of SideDrawer we track their mount/unmount using React's context. You must wrap your application with the <SideDrawerProvider/> for this to work correctly.

<Core>
<SideDrawerProvider>
<App />
</SideDrawerProvider>
</Core>

SideDrawerController

Each usage of a SideDrawer needs to be wrapped in a SideDrawerController. Generally, the usage of SideDrawerController WON'T look like it does in the live examples. The live examples are illustrating what you can do with them. The actual usage of the side drawers will look more like the code below. What's important to note here is that the SideDrawerController is outside of the component that renders the drawer. This is so that state is cleared out of the drawer when it's closed.

Good ✅

const SomeDrawer = () => {
return <SideDrawer>{/** ... content goes here */}</SideDrawer>;
};
return (
<SideDrawerController>
<SomeDrawer />
</SideDrawerController>
);

Bad ❌

return (
<SideDrawerController>
<SideDrawer>{/** ... content goes here */}</SideDrawer>
</SideDrawerController>
);

Opening and closing a side drawer

The consumer is responsible for tracking and updating open/closed states. SideDrawerController takes an isOpen prop to track the open/closed state of the drawer and an onClose prop which is used when the user presses Escape.

const [isOpen, toggleIsOpen] = React.useReducer((bool) => !bool, false);
return (
<SideDrawerController isOpen={isOpen} onClose={toggleIsOpen}>
{/** ... side drawer stuff goes here */}
</SideDrawerController>
);

Why can't I conditionally render?

We need the SideDrawerController component so that we can do an exit transition for the drawer. If you just did isOpen && <MyDrawer />, the drawer would unmount immediately so it wouldn't be possible to have an exit transition.

If you see a usage of the SideDrawerController in an app where it's the directly above a drawer, that will be updated. It's only like that to ease the transition period from isOpen being a prop on side drawers to being on the SideDrawerController.

SideDrawer

A side drawer is a modal dialog that slides in from the right side of the screen. They are used to display helpful information and small forms that support the users main activity without interrupting it.

  • Side drawers can stack on top of each other.
  • Side drawers should not contain data tables. Bottom drawers are more suitable for large, complex forms.
  • Side drawers, bottom drawers and product menus cannot be used together i.e a side drawer should never have a bottom drawer or a product menu inside it and vice versa.

Backgroud

SideDrawer supports base(default) and muted backgrounds via the background prop:

<SideDrawer background="muted"></SideDrawer>

Focus

By default the side drawer will focus the outter most element on open. It's possible to focus a different element instead by using the initialFocusRef prop. Check out the full example here.

Accessibility props

To maintain accessibility in scenarios when opting for a custom implementation of header and/or title, the aria-label and aria-labelledby props can be used. Check out the examples below for more details:

Composition

SideDrawer content is composed using a combination of primitives and custom implementations. The primitives cover the most common use cases and where they aren't sufficient, it's possible to opt out in favor of a custom implementation.

Side drawer primitives follow a component as namespace strategy i.e all the primitives relating to SideDrawer are exposed as SideDrawer.SomePrimitive. Below is a list of all the primitives, their purpose and some sample code.

The code samples are meant to convey the general idea, for concrete examples on composition using primitives and custom implementations check out the patterns section.

SideDrawer.Header

Used to display the title and some optional subtext or actions. To be used inside SideDrawer or SideDrawer.Form.

<SideDrawer>
<SideDrawer.Header>{/** ... header content goes here **/}</SideDrawer.Header>
</SideDrawer>

OR

<SideDrawer>
<SideDrawer.Form>
<SideDrawer.Header>
{/** ... header content goes here **/}
</SideDrawer.Header>
</SideDrawer.Form>
</SideDrawer>
Behaviour and styling

This is a container component that provides horizontal and vertical padding and renders children as flex row.

Variance, when and how to opt out?

The header is a convenience layout primitive and hence can be easily opted out of. However, there should be little reason to opt out of it entirely. In case of a full opt out, it's the consumers responsibility to provide proper padding and spacing between elements.

It's possible to compose a header UI containing a heading, description text and an action as below:

<>
<Stack flex={1} gap="xsmall">
<SideDrawer.Title>Title</SideDrawer.Title>
<Text color="muted" size="small">
Optional description text
</Text>
</Stack>
<IconButton
label="Delete"
size="small"
tone="critical"
weight="none"
icon={Trash2Icon}
onClick={onClose}
/>
</>

SideDrawer.Title

Title of the drawer. To be used inside SideDrawer.Header or custom header implementation.

<SideDrawer.Header>
<SideDrawer.Title>Title</SideDrawer.Title>
</SideDrawer.Header>
Variance, when and how to opt out?

The title primitive is a level 3 heading under the hood and it should suffice in most cases. It exists so that consumers don't have to guess the heading level every time. The title should be fairly consistent across most drawers and hence warrant little reason for opting out.

Custom implementations of title must be connected to the SideDrawer by using the aria-labelledby prop to support proper accessibility.

<SideDrawer aria-labelledby="custom-title">
<SideDrawer.Header>
<Heading id='custom-title' level="2">Large title</Heading>
</SideDrawer.Header>
<SideDrawer>

SideDrawer.Body

Container for the main drawer content. To be used inside SideDrawer or SideDrawer.Form

<SideDrawer>
<SideDrawer.Body>{/** ... scrollable content goes here */}</SideDrawer.Body>
</SideDrawer>

OR

<SideDrawer>
<SideDrawer.Form>
<SideDrawer.Body>{/** ... scrollable content goes here */}</SideDrawer.Body>
</SideDrawer.Form>
</SideDrawer>
Behaviour and styles

This is a container component that renders children as flex column. If the UI requires a row layout you can simply use a flex box inside SideDrawer.Body to wrap you content.

<SideDrawer>
<SideDrawer.Body>
<Flex>{/** ... content here is rendered as flex row now */}</Flex>
</SideDrawer.Body>
</SideDrawer>

It does not apply any padding, it's the consumer's responsibility to apply the correct padding to the body. In most cases, a Box with padding will suffice. Be sure to align the horizontal padding with the header content.

<SideDrawer>
<SideDrawer.Body>
<Box paddingX="xlarge" paddingY="large">
{/** ... we now have padding */}
</Box>
</SideDrawer.Body>
</SideDrawer>

Overflowing content is scrolled. When the body content is scrollable and user scrolls up, a fully bleed divider appears between the header and the body to improve affordance.

Variance, when and how to opt out?

Although the body content itself may have a lot of variance, it is not recommended to opt out of using SideDrawer.Body as it provides some critical scrolling and border behaviours. These behaviours are difficult to implement correctly and hence one should have very good reasons for opting out of this primitive.

SideDrawer.Footer

Container for the primary CTA of the drawer. To be used inside SideDrawer or SideDrawer.Form.

<SideDrawer>
<SideDrawer.Footer>{/** ... footer content */}</SideDrawer.Footer>
</SideDrawer>
<SideDrawer>
<SideDrawer.Form>
<SideDrawer.Footer>{/** ... footer content */}</SideDrawer.Footer>
</SideDrawer.Form>
</SideDrawer>
Behaviour and styles

This is a container component that provides horizontal and veritical padding. It renders it's children as flex column so it seamlessly accepts buttons as children.

<SideDrawer>
<SideDrawer.Footer>
<Button label="Done" onClick={() => {}}>
<Button label="Cancel" weight="none" onClick={() => {}}>
</SideDrawer.Footer>
</SideDrawer>
Variance, when and how to opt out?

The footer is a convenience layout primitive and hence can be opted out of easily. However, there should be little reason to entirely opt out of it entirely.

When rolling a customer implementation, it's the consumers responsibility to provide proper padding and spacing between elements.

SideDrawer.Form

A wrapper that connects the submit action in the footer to the form.

All of the drawer content and primitives must be wrapped inside SideDrawer.Form if the drawer is a form, otherwise your submit action will not work correctly.

Note: The submit function of the form must be passed to the onSubmit prop of SideDrawer.Form and there must be a button of type="submit" present in the footer.

<SideDrawer>
<SideDrawer.Form onSubmit={() => {}}>
<SideDrawer.Header>{/** ... header content goes here */}</SideDrawer.Header>
<SideDrawer.Body>{/** ... body content goes here */}</SideDrawer.Body>
<SideDrawer.Footer>
<Button type="submit" label="Done" />
</SideDrawer.Footer>
</SideDrawer>
</SideDrawer>

If the drawer is not a form then this component must not be.

Receipes

Below are some of the most common use cases of side drawers and how to compose them using primitives and custom code.

Basic form

A simple form to add/edit an entity and then return to the main flow.

Edit in Playroom

Composing content

Simple supporting text that offers an explanation of the users main flow.

Custom header actions

Custom actions such as delete in the header.

Custom title

When using a custom title instead of SideDrawer.Title, the aria-labelledby prop must be provided to SideDrawer.

Loading states

When display a loading state on initial load, the aria-label prop must be provided to SideDrawer till the title is rendered.

Focus a different element on open

By default, the outter most element will be focused when the drawer opens. You can provide an initialFocusRef to focus a different element instead.

Migrating from Drawer

The major difference between Drawer and SideDrawer is configuration vs composition. Drawers encourage the use of primitives to compose your UIs which offers more flexibility than prop based UIs. Below is a mapping of drawer props to drawer primitives.

  • title: SideDrawer.Header + SideDrawer.Title
  • header: SideDrawer.Header + SideDrawer.Title + custom code
  • actions: SideDrawer.Footer
  • type=form: SideDrawer.Form wraps the drawer content
  • type=content: SideDrawer.Body
  • shouldShowCancelConfirmationDialog: custom onSubmit function + useFormSnapshot hook + AlertDialog

Form drawer

Before ⛔️

After ✅

Content drawer

Before ⛔️

After ✅

Copyright © 2024 Reckon. Designed and developed in partnership with Thinkmill.
Bitbucket logoJira software logoConfluence logo