Skip to content

Drawer

A dialog that slides in from an edge, HeroUI's on Kobalte's.

Solidtype_heroui.Drawer is Kobalte’s Dialog sliding in from an edge of the page, with the classes HeroUI’s own drawerVariants gives. It is a Modal in all but these: its content slides in from the edge its placement names; its trigger is a button; and while it may be dismissed its dialog is dragged toward its edge to close, as HeroUI’s is.

drawer_usage.mlx
module Button = Solidtype_heroui.Button
module Drawer = Solidtype_heroui.Drawer
let make () =
<Drawer>
<Button variant=`secondary>"Open Drawer"</Button>
<Drawer.Backdrop>
<Drawer.Content placement=`right>
<Drawer.Dialog>
<Drawer.Header>
<Drawer.Heading>"Drawer Title"</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<p>
"This is a bottom drawer built with React Aria's Modal \
component. It slides up from the bottom of the screen with a \
smooth CSS transition."
</p>
</Drawer.Body>
<Drawer.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Confirm"</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>
<Drawer>
<Button>"Open Drawer"</Button>
<Drawer.Backdrop>
<Drawer.Content placement=`bottom>
<Drawer.Dialog>
<Drawer.Handle />
<Drawer.Close_trigger />
<Drawer.Header>
<Drawer.Heading>...</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>...</Drawer.Body>
<Drawer.Footer>...</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>
drawer_placement.mlx
module Jsx = Solidtype.Jsx
module For = Solidtype.For
module Show = Solidtype.Show
module Button = Solidtype_heroui.Button
module Drawer = Solidtype_heroui.Drawer
let placements =
[| (`bottom, "bottom"); (`top, "top"); (`left, "left"); (`right, "right") |]
let make () =
<div class_="flex flex-wrap gap-4">
<For each=(fun () -> placements)>
(fun (placement, name) _ ->
<Drawer>
<Button variant=`secondary>
(fun () -> Jsx.string (String.capitalize_ascii name))
</Button>
<Drawer.Backdrop>
<Drawer.Content placement=(fun () -> placement)>
<Drawer.Dialog>
<Drawer.Close_trigger />
<Show
when_=(fun () ->
match placement with
| `bottom -> true
| `top | `left | `right -> false)>
<Drawer.Handle />
</Show>
<Drawer.Header>
<Drawer.Heading>
(fun () -> Jsx.string (String.capitalize_ascii name))
" Drawer"
</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<p>
"This drawer slides in from the "
<strong>(fun () -> Jsx.string name)</strong>
" edge of the screen."
</p>
</Drawer.Body>
<Drawer.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Done"</Button>
</Drawer.Footer>
<Show
when_=(fun () ->
match placement with
| `top -> true
| `bottom | `left | `right -> false)>
<Drawer.Handle />
</Show>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>)
</For>
</div>
drawer_non_dismissable.mlx
module Button = Solidtype_heroui.Button
module Drawer = Solidtype_heroui.Drawer
let make () =
<Drawer>
<Button variant=`secondary>"Important Action"</Button>
<Drawer.Backdrop is_dismissable=false>
<Drawer.Content>
<Drawer.Dialog>
<Drawer.Header>
<Drawer.Heading>"Confirm Action"</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<p>
"This drawer cannot be dismissed by clicking outside or \
dragging. You must use one of the buttons below."
</p>
</Drawer.Body>
<Drawer.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Confirm"</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>
drawer_scrollable.mlx
module Jsx = Solidtype.Jsx
module For = Solidtype.For
module Button = Solidtype_heroui.Button
module Drawer = Solidtype_heroui.Drawer
let paragraphs = Array.init 20 (fun i -> i + 1)
let make () =
<Drawer>
<Button variant=`secondary>"Terms & Conditions"</Button>
<Drawer.Backdrop>
<Drawer.Content>
<Drawer.Dialog>
<Drawer.Handle />
<Drawer.Close_trigger />
<Drawer.Header>
<Drawer.Heading>"Terms & Conditions"</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<For each=(fun () -> paragraphs)>
(fun n _ ->
<p class_="mb-3">
"Paragraph "
(fun () -> Jsx.int n)
": Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
Nullam pulvinar risus non risus hendrerit venenatis. \
Pellentesque sit amet hendrerit risus, sed porttitor quam."
</p>)
</For>
</Drawer.Body>
<Drawer.Footer>
<Button slot=`close variant=`secondary>"Decline"</Button>
<Button slot=`close>"Accept"</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>

A signal is Solid’s state: each half keeps the drawer’s open state in one, where HeroUI’s uses React.useState and useOverlayState.

With a signal

Control the drawer using Solid's signal for simple state management.

Status: closed

With a signal's setter

Use the signal's setter for a cleaner API with convenient calls like S.set set_open true, S.set set_open false, and S.update set_open not.

Status: closed

drawer_controlled.mlx
module S = Solidtype.Solid
module Jsx = Solidtype.Jsx
module Button = Solidtype_heroui.Button
module Drawer = Solidtype_heroui.Drawer
(* HeroUI shows React's useState and its own useOverlayState; a signal is
Solid's state, so both halves keep the drawer's in one. *)
let make () =
let is_open, set_open = S.create_signal false in
let state_open, set_state_open = S.create_signal false in
<div class_="flex max-w-md flex-col gap-8">
<div class_="flex flex-col gap-3">
<h3 class_="text-lg font-semibold text-foreground">"With a signal"</h3>
<p class_="text-sm leading-relaxed text-pretty text-muted">
"Control the drawer using Solid's "
<code class_="text-foreground">"signal"</code>
" for simple state management."
</p>
<div
class_="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 \
shadow-sm">
<div class_="flex w-full items-center justify-between">
<p class_="text-xs text-muted">
"Status: "
<span class_="font-mono font-medium text-foreground">
(fun () ->
Jsx.string (if S.get is_open then "open" else "closed"))
</span>
</p>
</div>
<div class_="flex gap-2">
<Button
size=`sm variant=`secondary on_click=(fun _ -> S.set set_open true)>
"Open Drawer"
</Button>
<Button
size=`sm variant=`tertiary on_click=(fun _ -> S.update set_open not)>
"Toggle"
</Button>
</div>
</div>
<Drawer.Backdrop
is_open=(fun () -> S.get is_open) on_open_change=(S.set set_open)>
<Drawer.Content placement=`right>
<Drawer.Dialog>
<Drawer.Close_trigger />
<Drawer.Header>
<Drawer.Heading>"Controlled with a signal"</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<p>
"This drawer is controlled by Solid's "
<code>"signal"</code>
". Pass "
<code>"is_open"</code>
" and "
<code>"on_open_change"</code>
" props to manage the drawer state externally."
</p>
</Drawer.Body>
<Drawer.Footer>
<Button slot=`close variant=`secondary>"Close"</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</div>
<div class_="flex flex-col gap-3">
<h3 class_="text-lg font-semibold text-foreground">
"With a signal's setter"
</h3>
<p class_="text-sm leading-relaxed text-pretty text-muted">
"Use the "
<code class_="text-foreground">"signal"</code>
"'s setter for a cleaner API with convenient calls like "
<code>"S.set set_open true"</code>
", "
<code>"S.set set_open false"</code>
", and "
<code>"S.update set_open not"</code>
"."
</p>
<div
class_="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 \
shadow-sm">
<div class_="flex w-full items-center justify-between">
<p class_="text-xs text-muted">
"Status: "
<span class_="font-mono font-medium text-foreground">
(fun () ->
Jsx.string (if S.get state_open then "open" else "closed"))
</span>
</p>
</div>
<div class_="flex gap-2">
<Button
size=`sm
variant=`secondary
on_click=(fun _ -> S.set set_state_open true)>
"Open Drawer"
</Button>
<Button
size=`sm
variant=`tertiary
on_click=(fun _ -> S.update set_state_open not)>
"Toggle"
</Button>
</div>
</div>
<Drawer.Backdrop
is_open=(fun () -> S.get state_open)
on_open_change=(S.set set_state_open)>
<Drawer.Content placement=`right>
<Drawer.Dialog>
<Drawer.Close_trigger />
<Drawer.Header>
<Drawer.Heading>
"Controlled with a signal's setter"
</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<p>
"The "
<code>"signal"</code>
{js|'s setter covers the common operations. No need to manually create callbacks—just use |js}
<code>"S.set set_open true"</code>
", "
<code>"S.set set_open false"</code>
", or "
<code>"S.update set_open not"</code>
"."
</p>
</Drawer.Body>
<Drawer.Footer>
<Button slot=`close variant=`secondary>"Close"</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</div>
</div>
drawer_form.mlx
module Button = Solidtype_heroui.Button
module Drawer = Solidtype_heroui.Drawer
module Input = Solidtype_heroui.Input
module Label = Solidtype_heroui.Label
module Text_field = Solidtype_heroui.Text_field
let make () =
<Drawer>
<Button variant=`secondary>"Edit Profile"</Button>
<Drawer.Backdrop>
<Drawer.Content placement=`right>
<Drawer.Dialog>
<Drawer.Close_trigger />
<Drawer.Header>
<Drawer.Heading>"Edit Profile"</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<form class_="flex flex-col gap-4">
<Text_field class_="w-full" name="name">
<Label>"Name"</Label>
<Input
type_="text" placeholder="Enter your name" variant=`secondary
/>
</Text_field>
<Text_field class_="w-full" name="email">
<Label>"Email"</Label>
<Input
type_="email"
placeholder="Enter your email"
variant=`secondary
/>
</Text_field>
<Text_field class_="w-full" name="bio">
<Label>"Bio"</Label>
<Input placeholder="Tell us about yourself" variant=`secondary
/>
</Text_field>
</form>
</Drawer.Body>
<Drawer.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Save Changes"</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>
drawer_navigation.mlx
module Jsx = Solidtype.Jsx
module For = Solidtype.For
module Button = Solidtype_heroui.Button
module Drawer = Solidtype_heroui.Drawer
let nav_items =
[|
((fun () -> <Gravity.House class_="size-5 text-muted" />), "Home");
((fun () -> <Gravity.Magnifier class_="size-5 text-muted" />), "Search");
((fun () -> <Gravity.Bell class_="size-5 text-muted" />), "Notifications");
((fun () -> <Gravity.Envelope class_="size-5 text-muted" />), "Messages");
((fun () -> <Gravity.Person class_="size-5 text-muted" />), "Profile");
((fun () -> <Gravity.Gear class_="size-5 text-muted" />), "Settings");
|]
let make () =
<Drawer>
<Button variant=`secondary><Gravity.Bars /> "Menu"</Button>
<Drawer.Backdrop>
<Drawer.Content placement=`left>
<Drawer.Dialog>
<Drawer.Close_trigger />
<Drawer.Header>
<Drawer.Heading>"Navigation"</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<nav class_="flex flex-col gap-1">
<For each=(fun () -> nav_items)>
(fun (icon, label) _ ->
<button
class_="flex items-center gap-3 rounded-xl px-3 py-2.5 \
text-sm text-foreground transition-colors \
hover:bg-default"
type_=`button>
(fun () -> Jsx.element (icon ()))
(fun () -> Jsx.string label)
</button>)
</For>
</nav>
</Drawer.Body>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>
drawer_backdrop_variants.mlx
module Jsx = Solidtype.Jsx
module For = Solidtype.For
module Button = Solidtype_heroui.Button
module Drawer = Solidtype_heroui.Drawer
let variants =
[| (`opaque, "opaque"); (`blur, "blur"); (`transparent, "transparent") |]
let make () =
<div class_="flex flex-wrap gap-4">
<For each=(fun () -> variants)>
(fun (variant, name) _ ->
<Drawer>
<Button variant=`secondary>
(fun () -> Jsx.string (String.capitalize_ascii name))
</Button>
<Drawer.Backdrop variant=(fun () -> variant)>
<Drawer.Content>
<Drawer.Dialog>
<Drawer.Handle />
<Drawer.Close_trigger />
<Drawer.Header>
<Drawer.Heading>
"Backdrop: "
(fun () -> Jsx.string (String.capitalize_ascii name))
</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<p>
"This drawer uses the "
<code>(fun () -> Jsx.string name)</code>
" backdrop variant."
</p>
</Drawer.Body>
<Drawer.Footer>
<Button class_="w-full" slot=`close>"Close"</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>)
</For>
</div>

class_ is added after HeroUI’s classes.

drawer_tailwind.mlx
module Button = Solidtype_heroui.Button
module Drawer = Solidtype_heroui.Drawer
let make () =
<Drawer>
<Button variant=`secondary>"Open filters"</Button>
<Drawer.Backdrop variant=`blur>
<Drawer.Content placement=`right>
<Drawer.Dialog class_="border-l border-border/80 bg-surface">
<Drawer.Header>
<Drawer.Heading class_="text-foreground">"Filters"</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<p class_="text-sm text-muted">
"Narrow results by status, owner, or date."
</p>
</Drawer.Body>
<Drawer.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Apply"</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>

HeroUI’s classes are global, so a stylesheet restyles every one, as with HeroUI in React:

@layer components {
.drawer__dialog {
@apply rounded-t-3xl;
}
.drawer__heading {
@apply text-lg font-semibold;
}
}
  • .drawer__trigger, .drawer__backdrop, .drawer__content, .drawer__dialog, .drawer__handle, .drawer__header, .drawer__heading, .drawer__body, .drawer__footer, .drawer__close-trigger
  • .drawer__backdrop--opaque, --blur, --transparent
  • .drawer__content--bottom, --top, --left, --right, and the same of .drawer__dialog
  • Entering: [data-entering="true"] on the backdrop and the content
  • Exiting: [data-exiting="true"] on the backdrop and the content
  • Placement: [data-placement="bottom"], and the other edges, on the content and the dialog

It shares Modal’s making, on Kobalte’s dialog: see Modal. Its drag is HeroUI’s: from anywhere on its dialog but a button, a field, a link or its body, it starts past 8 pixels, follows the pointer toward the drawer’s edge alone, and on release closes the drawer past 30% of its size or at a flick faster than half a pixel a millisecond, leaving the drawer where it was let go to slide on out, else springs back. The dialog’s own slide in and out is waited for, as its backdrop’s and content’s are.

Each prop beside the name HeroUI’s React component gives it.

React solidtype Type Default Description
isOpen is_open bool Whether it is open, controlled
defaultOpen default_open bool false Whether it starts open
onOpenChange on_open_change bool -> unit Told as it opens or closes
React solidtype Type Default Description
variant variant `opaque | `blur | `transparent `opaque Its look
isDismissable is_dismissable bool true Whether a press on it, or a drag, closes the drawer
isKeyboardDismissDisabled is_keyboard_dismiss_disabled bool false Whether Escape does not
isOpen is_open bool With no Drawer around it, whether it is open
onOpenChange on_open_change bool -> unit With no Drawer around it, told as it opens or closes
className class_ string Added after HeroUI’s classes
React solidtype Type Default Description
placement placement `top | `bottom | `left | `right `bottom The edge it slides from
className class_ string Added after HeroUI’s classes
Section titled “Drawer.Trigger, Dialog, Header, Body, Footer, Heading, Handle, CloseTrigger”
React solidtype Type Description
className class_ string Added after HeroUI’s classes
children children elements What it holds, where it holds any

Original HeroUI documentation: Drawer.

The same HeroUI Drawer twice, under the same props: in solidtype on the left, in React on the right. Below them, every attribute of each trigger: a marked row is one they disagree on, and a dimmed row is a framework’s own, which no style reads.

solidtype — OCaml, on Solid and Kobalte
React — TypeScript, on React Aria
attributesolidtypeReact