Drawer
A drawer panel that slides in from the left edge of the screen, used for navigation menus, filters, or secondary content.
Docsimport {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerTitle,
DrawerDescription,
} from "~/components/drawer";
export default function DrawerBasicDemo() {
return (
<div>
<Drawer swipeDirection="start">
<DrawerTrigger>Open Drawer</DrawerTrigger>
<DrawerContent>
<DrawerTitle>Title</DrawerTitle>
<DrawerDescription>Description text</DrawerDescription>
</DrawerContent>
</Drawer>
</div>
);
}Installation
CLI
Run the following command to add the component to your project:
npx @ark-preset/cli@latest add drawerManual
Create the recipe file at src/components/recipes/drawer.ts:
import { tv, type VariantProps } from "tailwind-variants";
export const drawerVariants = tv({
slots: {
root: "",
trigger:
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition duration-300 motion-safe:hover:-translate-y-0.5 motion-safe:active:translate-y-0.5 active:duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border bg-background hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2",
backdrop:
"fixed inset-0 z-50 bg-black/50 dark:bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
positioner:
"fixed inset-0 z-50 flex data-[swipe-direction=left]:justify-start data-[swipe-direction=right]:justify-end data-[swipe-direction=up]:items-start data-[swipe-direction=up]:justify-center data-[swipe-direction=down]:items-end data-[swipe-direction=down]:justify-center",
content:
"relative z-50 flex flex-col w-full max-w-md bg-background shadow-lg p-2 data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe-direction=left]:data-[state=open]:slide-in-from-left data-[swipe-direction=left]:data-[state=closed]:slide-out-to-left data-[swipe-direction=right]:data-[state=open]:slide-in-from-right data-[swipe-direction=right]:data-[state=closed]:slide-out-to-right",
title: "text-lg font-semibold tracking-tight text-left",
description: "text-sm text-muted-foreground text-left",
closeTrigger:
"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
grabber:
"absolute right-0 top-1/2 -translate-y-1/2 flex items-center justify-center cursor-grab w-6 h-12 z-10",
grabberIndicator: "w-1 h-8 bg-muted-foreground/30 rounded-full",
},
});
export type DrawerVariants = VariantProps<typeof drawerVariants>;Create the component directory and files.
src/components/drawer/drawer.base.tsx:
import { Drawer as ArkDrawer } from "@ark-ui/solid/drawer";
import { splitProps, type Component } from "solid-js";
import { buttonVariants, drawerVariants, type ButtonVariants } from "../recipes/drawer";
const styles = drawerVariants();
const Root = ArkDrawer.Root;
const RootProvider = ArkDrawer.RootProvider;
const Trigger: Component<ArkDrawer.TriggerProps & ButtonVariants> = (props) => {
const [local, others] = splitProps(props, ["class", "variant", "size"]);
return (
<ArkDrawer.Trigger
class={buttonVariants({ variant: local.variant, size: local.size, class: local.class })}
{...others}
/>
);
};
const Backdrop: Component<ArkDrawer.BackdropProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkDrawer.Backdrop class={styles.backdrop({ class: local.class })} {...others} />;
};
const Positioner: Component<ArkDrawer.PositionerProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkDrawer.Positioner class={styles.positioner({ class: local.class })} {...others} />;
};
const Content: Component<ArkDrawer.ContentProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkDrawer.Content class={styles.content({ class: local.class })} {...others} />;
};
const CloseTrigger: Component<ArkDrawer.CloseTriggerProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkDrawer.CloseTrigger class={styles.closeTrigger({ class: local.class })} {...others} />;
};
const Title: Component<ArkDrawer.TitleProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkDrawer.Title class={styles.title({ class: local.class })} {...others} />;
};
const Description: Component<ArkDrawer.DescriptionProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkDrawer.Description class={styles.description({ class: local.class })} {...others} />;
};
const Grabber: Component<ArkDrawer.GrabberProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkDrawer.Grabber class={styles.grabber({ class: local.class })} {...others} />;
};
const GrabberIndicator: Component<ArkDrawer.GrabberIndicatorProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<ArkDrawer.GrabberIndicator
class={styles.grabberIndicator({ class: local.class })}
{...others}
/>
);
};
const Context = ArkDrawer.Context;
const Indent = ArkDrawer.Indent;
const IndentBackground = ArkDrawer.IndentBackground;
const SwipeArea = ArkDrawer.SwipeArea;
export const Drawer = {
Root,
RootProvider,
Trigger,
Backdrop,
Positioner,
Content,
CloseTrigger,
Title,
Description,
Grabber,
GrabberIndicator,
Context,
Indent,
IndentBackground,
SwipeArea,
};src/components/drawer/index.tsx:
import { Drawer as ArkDrawer } from "@ark-ui/solid/drawer";
import { Portal } from "solid-js/web";
import { splitProps, type Component } from "solid-js";
import { Drawer as DrawerBase } from "./drawer.base";
const DrawerContent: Component<ArkDrawer.ContentProps> = (props) => {
const [local, others] = splitProps(props, ["class", "children"]);
return (
<Portal>
<DrawerBase.Backdrop />
<DrawerBase.Positioner>
<DrawerBase.Content class={local.class} {...others}>
{local.children}
<DrawerBase.CloseTrigger>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
class="size-4"
>
<path d="M18 6L6 18" />
<path d="M6 6l12 12" />
</svg>
</DrawerBase.CloseTrigger>
</DrawerBase.Content>
</DrawerBase.Positioner>
</Portal>
);
};
const Drawer = DrawerBase.Root;
const DrawerTrigger = DrawerBase.Trigger;
const DrawerTitle = DrawerBase.Title;
const DrawerDescription = DrawerBase.Description;
const DrawerGrabber: Component<ArkDrawer.GrabberProps> = (props) => {
const [local, others] = splitProps(props, ["class", "children"]);
return (
<DrawerBase.Grabber class={local.class} {...others}>
<DrawerBase.GrabberIndicator />
</DrawerBase.Grabber>
);
};
export {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerTitle,
DrawerDescription,
DrawerGrabber,
DrawerBase,
};
export { drawerVariants, type DrawerVariants } from "../recipes/drawer";Note: Make sure your project has the Tailwind CSS theme variables set up (
--background,--foreground,--ring,--border, etc.) or override the utility classes to match your design system.
Dependencies: This component imports shared recipes or sub-components from other packages. Make sure the following are also installed: button.
Usage
Basic Usage
A basic drawer that slides in from the screen edge.
import {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerTitle,
DrawerDescription,
} from "~/components/drawer";
export default function DrawerBasicDemo() {
return (
<div>
<Drawer swipeDirection="start">
<DrawerTrigger>Open Drawer</DrawerTrigger>
<DrawerContent>
<DrawerTitle>Title</DrawerTitle>
<DrawerDescription>Description text</DrawerDescription>
</DrawerContent>
</Drawer>
</div>
);
}With Form
Combine with form elements for profile editing, settings, or data entry.
import {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerTitle,
DrawerDescription,
Input,
} from "~/components/drawer";
export default function DrawerWithFormDemo() {
return (
<div>
<Drawer swipeDirection="start">
<DrawerTrigger>Edit Profile</DrawerTrigger>
<DrawerContent>
<div class="flex flex-col gap-1 px-4 pt-2 pb-4">
<DrawerTitle>Edit Profile</DrawerTitle>
<DrawerDescription>Update your personal information.</DrawerDescription>
</div>
<div class="flex flex-col gap-4 p-4">
<Input label="Name" placeholder="Your name" />
<Input label="Email" type="email" placeholder="your@email.com" />
</div>
</DrawerContent>
</Drawer>
</div>
);
}Advanced Usage
Root Provider
Use DrawerBase.RootProvider when you need to access the drawer state outside of the component tree. This pattern uses the useDrawer hook from Ark UI to create a shared context.
import { useDrawer } from "@ark-ui/solid/drawer";
import {
DrawerContent,
DrawerTrigger,
DrawerTitle,
DrawerDescription,
DrawerBase,
Button,
} from "~/components/drawer";
export default function DrawerRootProviderDemo() {
const drawer = useDrawer({ defaultOpen: false });
return (
<div>
<Button onClick={() => drawer().setOpen(true)}>Open Drawer</Button>
<DrawerBase.RootProvider value={drawer}>
<DrawerTrigger style="display:none">Hidden Trigger</DrawerTrigger>
<DrawerContent>
<DrawerTitle>Externally Controlled Drawer</DrawerTitle>
<DrawerDescription>This drawer is controlled via useDrawer.</DrawerDescription>
</DrawerContent>
</DrawerBase.RootProvider>
</div>
);
}The key difference:
Drawer— manages its own state internally. Use for simple, self-contained usage.DrawerBase.RootProvider— accepts a pre-created context viauseDrawer. Use when you need to read or control the drawer state from outside the component tree.DrawerBase.Context— a render-prop component that provides access to the drawer state. Use when you need to read the open/close state inside the drawer tree.
Example using DrawerBase.Context:
import { Drawer, DrawerTrigger, DrawerContent, DrawerBase } from "~/components/drawer";
import { Button } from "~/components/button";
<Drawer>
<DrawerTrigger>Open</DrawerTrigger>
<DrawerContent>
<DrawerBase.Context>
{(drawer) => <p>Drawer is {drawer().open ? "open" : "closed"}</p>}
</DrawerBase.Context>
</DrawerContent>
</Drawer>API Reference
See the Ark UI Drawer documentation.