You are looking at the documentation of the work in progress Hope UI 1.0, examples and information may be broken or outdated.

Overlays

Drawer

Display a modal dialog that slides out from the edge of the screen.

Import

tsx
import { Drawer } from "@hope-ui/core";
tsx
import { Drawer } from "@hope-ui/core";
  • Drawer: The wrapper that provides props, state, and context to its children.
  • Drawer.Overlay: The backdrop behind the drawer.
  • Drawer.Content: The drawer itself.
  • Drawer.CloseButton: A button to close the drawer.
  • Drawer.Heading: An optional heading for the drawer.
  • Drawer.Description: An optional description for the drawer.

Usage

tsx
function BasicUsageExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}
tsx
function BasicUsageExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}

Initial focus

When the drawer opens, focus is automatically set on the first focusable element in Drawer.Content.

Use the data-autofocus attribute on any element to mark it as the initial focus element.

tsx
function InitialFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<Text mb={4}>The content of the Drawer.</Text>
<HStack justifyContent="flex-end" spacing={4}>
<Button data-autofocus _focus={{ color: "red" }}>
Action
</Button>
</HStack>
</Drawer.Content>
</Drawer>
</>
);
}
tsx
function InitialFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<Text mb={4}>The content of the Drawer.</Text>
<HStack justifyContent="flex-end" spacing={4}>
<Button data-autofocus _focus={{ color: "red" }}>
Action
</Button>
</HStack>
</Drawer.Content>
</Drawer>
</>
);
}

You can also use a different css selector by using the initialFocusSelector prop.

tsx
function CustomInitialFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
initialFocusSelector="#initial-focus"
>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<Text mb={4}>The content of the Drawer.</Text>
<HStack justifyContent="flex-end" spacing={4}>
<Button id="initial-focus" _focus={{ color: "red" }}>
Action
</Button>
</HStack>
</Drawer.Content>
</Drawer>
</>
);
}
tsx
function CustomInitialFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
initialFocusSelector="#initial-focus"
>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<Text mb={4}>The content of the Drawer.</Text>
<HStack justifyContent="flex-end" spacing={4}>
<Button id="initial-focus" _focus={{ color: "red" }}>
Action
</Button>
</HStack>
</Drawer.Content>
</Drawer>
</>
);
}

If the drawer contains no focusable element, focus is set to the Drawer.Content itself.

Restore focus

When the drawer closes, focus is set back to the trigger element. Use the restoreFocusSelector prop and pass in a css selector to target another element.

tsx
function CustomRestoreFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<HStack spacing={4}>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
restoreFocusSelector="[data-finalfocus]"
>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
<Button data-finalfocus _focus={{ color: "red" }}>
Final focus element
</Button>
</HStack>
);
}
tsx
function CustomRestoreFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<HStack spacing={4}>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
restoreFocusSelector="[data-finalfocus]"
>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
<Button data-finalfocus _focus={{ color: "red" }}>
Final focus element
</Button>
</HStack>
);
}

Focus trap

By default, focus is lock inside the drawer for accessibility reason. Set the trapFocus prop to false if you want to disable this behavior.

tsx
function DisableFocusTrapExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)} trapFocus={false}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}
tsx
function DisableFocusTrapExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)} trapFocus={false}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}

Prevent scroll

For accessibility reason scrolling on the main document behind the drawer is blocked by default. Set the preventScroll prop to false to allow scrolling behind the drawer.

tsx
function DisablePreventScrollExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)} preventScroll={false}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}
tsx
function DisablePreventScrollExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)} preventScroll={false}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}

Heading and description

Use Drawer.Heading to add an accessible heading to the drawer, it renders an h2 by default.

Similarly Drawer.Description is used add an accessible description and renders a p by default.

Under the hood, Hope UI will set the correct aria-labelledby and aria-describedby attributes.

tsx
function HeadingAndDescriptionExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack alignItems="flex-start" justifyContent="space-between" mb={4}>
<VStack alignItems="flex-start">
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.Description
fontSize="sm"
color={{ light: "neutral.600", dark: "neutral.300" }}
mb={4}
>
Description
</Drawer.Description>
</VStack>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}
tsx
function HeadingAndDescriptionExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack alignItems="flex-start" justifyContent="space-between" mb={4}>
<VStack alignItems="flex-start">
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.Description
fontSize="sm"
color={{ light: "neutral.600", dark: "neutral.300" }}
mb={4}
>
Description
</Drawer.Description>
</VStack>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}

Open/close transitions

The contentTransitionOptions and overlayTransitionOptions props allows to customize the drawer content and overlay open/close transitions. You can use predefined transitions or create custom ones.

tsx
function TransitionExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
contentTransitionOptions={{
transition: "slide-up",
duration: 400,
exitDuration: 250,
easing: "ease-out",
exitEasing: "ease-in",
}}
>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}
tsx
function TransitionExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
contentTransitionOptions={{
transition: "slide-up",
duration: 400,
exitDuration: 250,
easing: "ease-out",
exitEasing: "ease-in",
}}
>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}

To learn more about the transition API check out the createTransition documentation.

Placement

To change the edge where the Drawer should appear, use the placement prop.

tsx
function PlacementExample() {
const [placement, setPlacement] = createSignal<DrawerProps["placement"]>("right");
const [isOpen, setIsOpen] = createSignal(false);
const handleClick = (newPlacement: DrawerProps["placement"]) => {
setPlacement(newPlacement);
setIsOpen(true);
};
const placements: Array<DrawerProps["placement"]> = ["top", "right", "bottom", "left"];
return (
<>
<HStack spacing={4}>
<For each={placements}>
{placement => <Button onClick={() => handleClick(placement)}>{placement}</Button>}
</For>
</HStack>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)} placement={placement()}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}
tsx
function PlacementExample() {
const [placement, setPlacement] = createSignal<DrawerProps["placement"]>("right");
const [isOpen, setIsOpen] = createSignal(false);
const handleClick = (newPlacement: DrawerProps["placement"]) => {
setPlacement(newPlacement);
setIsOpen(true);
};
const placements: Array<DrawerProps["placement"]> = ["top", "right", "bottom", "left"];
return (
<>
<HStack spacing={4}>
<For each={placements}>
{placement => <Button onClick={() => handleClick(placement)}>{placement}</Button>}
</For>
</HStack>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)} placement={placement()}>
<Drawer.Overlay />
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}

Size

You can change the drawer width by setting the size prop.

tsx
function SizeExample() {
const [size, setSize] = createSignal<DrawerProps["size"]>("md");
const [isOpen, setIsOpen] = createSignal(false);
const handleClick = (newSize: DrawerProps["size"]) => {
setSize(newSize);
setIsOpen(true);
};
const sizes: Array<DrawerProps["size"]> = ["xs", "sm", "md", "lg", "xl", "full"];
return (
<>
<HStack spacing={4}>
<For each={sizes}>{size => <Button onClick={() => handleClick(size)}>{size}</Button>}</For>
</HStack>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)} size={size()}>
<Show when={size() !== "full"}>
<Drawer.Overlay />
</Show>
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}
tsx
function SizeExample() {
const [size, setSize] = createSignal<DrawerProps["size"]>("md");
const [isOpen, setIsOpen] = createSignal(false);
const handleClick = (newSize: DrawerProps["size"]) => {
setSize(newSize);
setIsOpen(true);
};
const sizes: Array<DrawerProps["size"]> = ["xs", "sm", "md", "lg", "xl", "full"];
return (
<>
<HStack spacing={4}>
<For each={sizes}>{size => <Button onClick={() => handleClick(size)}>{size}</Button>}</For>
</HStack>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)} size={size()}>
<Show when={size() !== "full"}>
<Drawer.Overlay />
</Show>
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}
The size prop has no effect when the placement is top or bottom.

Styling the backdrop

You can use style props and sx on the Drawer.Overlay component to customize the drawer backdrop.

tsx
function CustomBackdropExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Drawer.Overlay
sx={{
bg: "blackAlpha.300",
backdropFilter: "blur(10px) hue-rotate(90deg)",
}}
/>
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}
tsx
function CustomBackdropExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Drawer.Overlay
sx={{
bg: "blackAlpha.300",
backdropFilter: "blur(10px) hue-rotate(90deg)",
}}
/>
<Drawer.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Drawer.Heading fontWeight="semibold">Title</Drawer.Heading>
<Drawer.CloseButton />
</HStack>
<p>The content of the Drawer.</p>
</Drawer.Content>
</Drawer>
</>
);
}

Please be aware that not every browser supports the backdrop-filter CSS property used in the example above.

Accessibility

ARIA roles and attributes

  • The Drawer.Content has aria-drawer set to true.
  • The Drawer.Content has aria-labelledby set to the id of the Drawer.Heading, if any.
  • The Drawer.Content has aria-describedby set to the id of the Drawer.Description, if any.

Keyboard support and focus management

  • If the trapFocus prop is true, focus is locked within the drawer.
  • If the initialFocusSelector prop is set, focus is moved to the matching element, otherwise focus moves to the Drawer.Content.
  • When the drawer closes, focus returns to the trigger element unless another focusable element takes focus.
  • If the closeOnOverlayClick prop is true, clicking on the overlay closes the drawer.
  • When focus is within the Drawer.Content and closeOnEsc prop is true, pressing esc closes the drawer.
  • If the preventScroll prop is true, scrolling is blocked on the elements behind the drawer.

Theming

Use the DrawerTheme type to override Drawer styles and default props when extending Hope UI theme.

tsx
import { extendTheme, DrawerTheme } from "@hope-ui/core";
const theme = extendTheme({
components: {
Drawer: {
// ...overrides
} as DrawerTheme,
},
});
tsx
import { extendTheme, DrawerTheme } from "@hope-ui/core";
const theme = extendTheme({
components: {
Drawer: {
// ...overrides
} as DrawerTheme,
},
});

Component parts

NameStatic selectorDescription
roothope-Drawer-rootRoot element
contenthope-Drawer-contentDrawer content
overlayhope-Drawer-overlayDrawer overlay/backdrop
headinghope-Drawer-headingDrawer heading
descriptionhope-Drawer-descriptionDrawer description

Props

Drawer

NameTypeDescriptionDefault value
isOpen*booleanWhether the drawer should be shown.
onClose*() => voidA function that will be called to close the drawer.
idstringThe id of the drawer content.
size"xs" | "sm" | "md" | "lg" | "xl" | "full"The size of the drawer.
placement"top" | "right" | "bottom" | "left"The placement of the drawerright
contentTransitionOptionsTransitionOptionsOptions passed to the drawer content transition.
overlayTransitionOptionsTransitionOptionsOptions passed to the overlay transition.
closeOnOverlayClickbooleanWhether the drawer should close when the overlay is clicked.true
closeOnEscbooleanWhether the drawer should close when the user hit the Esc key.true
preventScrollbooleanWhether the scroll should be disabled on the body when the drawer opens.true
trapFocusbooleanWhether the focus will be locked into the drawer.true
initialFocusSelectorstringA query selector to retrieve the element that should receive focus once the drawer opens.
restoreFocusSelectorstringA query selector to retrieve the element that should receive focus once the drawer closes.
onOverlayClick() => voidA function that will be called when the overlay is clicked.
onEscKeyDown() => voidA function that will be called when the Esc key is pressed and focus is within the drawer.

Other components props

  • Drawer.CloseButton renders a CloseButton and supports all of its props.
  • Drawer.Overlay, Drawer.Content, Drawer.Heading and Drawer.Description supports commons Hope UI props (style props, sx and as).
Previous
Anchor
Next
Modal