1import { createSignal } from "solid-js";2import MinusIcon from "lucide-solid/icons/minus";3import PlusIcon from "lucide-solid/icons/plus";4
5import { Button } from "~/components/ui/button";6import {7 Drawer,8 DrawerClose,9 DrawerContent,10 DrawerDescription,11 DrawerFooter,12 DrawerHeader,13 DrawerTitle,14 DrawerTrigger,15} from "~/components/ui/drawer";16
17export default function DrawerDemo() {18 const [goal, setGoal] = createSignal(250);19
20 const onClick = (change: number) => {21 setGoal(goal() + change);22 };23
24 return (25 <Drawer>26 <DrawerTrigger as={Button<"button">}>Open Drawer</DrawerTrigger>27 <DrawerContent>28 <div class="mx-auto w-full max-w-sm">29 <DrawerHeader>30 <DrawerTitle>Move Goal</DrawerTitle>31 <DrawerDescription>Set your daily activity goal.</DrawerDescription>32 </DrawerHeader>33 <div class="p-4 pb-0">34 <div class="flex items-center justify-center space-x-2">35 <Button36 variant="outline"37 size="icon"38 class="size-8 shrink-0 rounded-full"39 onClick={() => onClick(-10)}40 disabled={goal() <= 200}41 >42 <MinusIcon class="size-4" />43 <span class="sr-only">Decrease</span>44 </Button>45 <div class="flex-1 text-center">46 <div class="text-7xl font-bold tracking-tighter">{goal()}</div>47 <div class="text-[0.70rem] text-muted-foreground uppercase">48 Calories/day49 </div>50 </div>51 <Button52 variant="outline"53 size="icon"54 class="size-8 shrink-0 rounded-full"55 onClick={() => onClick(10)}56 disabled={goal() >= 400}57 >58 <PlusIcon class="size-4" />59 <span class="sr-only">Increase</span>60 </Button>61 </div>62 </div>63 <DrawerFooter>64 <Button>Submit</Button>65 <DrawerClose as={Button<"button">} variant="outline">66 Cancel67 </DrawerClose>68 </DrawerFooter>69 </div>70 </DrawerContent>71 </Drawer>72 );73}
npx shadcn@latest add https://solid-ui-neobrutalism.vercel.app/r/drawer.json
yarn shadcn@latest add https://solid-ui-neobrutalism.vercel.app/r/drawer.json
pnpm dlx shadcn@latest add https://solid-ui-neobrutalism.vercel.app/r/drawer.json
bunx --bun shadcn@latest add https://solid-ui-neobrutalism.vercel.app/r/drawer.json
Install the following dependencies
npm install @corvu/drawer
yarn add @corvu/drawer
pnpm add @corvu/drawer
bun add @corvu/drawer
Copy and paste the following code into your project
1import type {2 CloseProps,3 ContentProps,4 DescriptionProps,5 DynamicProps,6 LabelProps,7 OverlayProps,8 PortalProps,9 RootProps,10 TriggerProps,11} from "@corvu/drawer";12import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js";13import type { Portal } from "solid-js/web";14
15import { splitProps } from "solid-js";16import DrawerPrimitive from "@corvu/drawer";17
18import { cn } from "~/lib/utils";19
20// const Drawer = DrawerPrimitive;21const Drawer = (props: RootProps) => {22 return <DrawerPrimitive data-slot="drawer" {...props} />;23};24
25const DrawerTrigger = <T extends ValidComponent = "button">(26 props: DynamicProps<T, TriggerProps<T>>,27) => {28 return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;29};30
31const DrawerPortal = (props: PortalProps & ComponentProps<typeof Portal>) => {32 return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;33};34
35const DrawerClose = <T extends ValidComponent = "button">(36 props: DynamicProps<T, CloseProps<T>>,37) => {38 return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;39};40
41type DrawerOverlayProps<T extends ValidComponent = "div"> = OverlayProps<T> & {42 class?: string;43};44
45const DrawerOverlay = <T extends ValidComponent = "div">(46 props: DynamicProps<T, DrawerOverlayProps<T>>,47) => {48 const [, rest] = splitProps(props as DrawerOverlayProps, ["class"]);49 const drawerContext = DrawerPrimitive.useContext();50 return (51 <DrawerPrimitive.Overlay52 data-slot="drawer-overlay"53 data-transitioning={drawerContext.openPercentage() * 100}54 class={cn(55 "fixed inset-0 z-50 data-[transitioning]:transition-colors data-[transitioning]:duration-300",56 props.class,57 )}58 style={{59 "background-color": `color-mix(in oklab, var(--overlay) ${100 * drawerContext.openPercentage()}%, transparent)`,60 }}61 {...rest}62 />63 );64};65
66type DrawerContentProps<T extends ValidComponent = "div"> = ContentProps<T> & {67 class?: string;68 children?: JSX.Element;69};70
71const DrawerContent = <T extends ValidComponent = "div">(72 props: DynamicProps<T, DrawerContentProps<T>>,73) => {74 const [, rest] = splitProps(props as DrawerContentProps, [75 "class",76 "children",77 ]);78 return (79 <DrawerPortal>80 <DrawerOverlay />81 <DrawerPrimitive.Content82 data-slot="drawer-content"83 class={cn(84 "group/drawer-content fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background after:absolute after:inset-x-0 after:top-full after:h-1/2 after:bg-inherit data-[transitioning]:transition-transform data-[transitioning]:duration-300 md:select-none",85 props.class,86 )}87 {...rest}88 >89 <div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-current" />90 {props.children}91 </DrawerPrimitive.Content>92 </DrawerPortal>93 );94};95
96const DrawerHeader: Component<ComponentProps<"div">> = (props) => {97 const [, rest] = splitProps(props, ["class"]);98 return (99 <div100 data-slot="drawer-header"101 class={cn("grid gap-1.5 p-4 text-center sm:text-left", props.class)}102 {...rest}103 />104 );105};106
107const DrawerFooter: Component<ComponentProps<"div">> = (props) => {108 const [, rest] = splitProps(props, ["class"]);109 return (110 <div111 data-slot="drawer-footer"112 class={cn("mt-auto flex flex-col gap-2 p-4", props.class)}113 {...rest}114 />115 );116};117
118type DrawerTitleProps<T extends ValidComponent = "div"> = LabelProps<T> & {119 class?: string;120};121
122const DrawerTitle = <T extends ValidComponent = "div">(123 props: DynamicProps<T, DrawerTitleProps<T>>,124) => {125 const [, rest] = splitProps(props as DrawerTitleProps, ["class"]);126 return (127 <DrawerPrimitive.Label128 data-slot="drawer-title"129 class={cn(130 "text-lg leading-none font-semibold tracking-tight",131 props.class,132 )}133 {...rest}134 />135 );136};137
138type DrawerDescriptionProps<T extends ValidComponent = "div"> =139 DescriptionProps<T> & {140 class?: string;141 };142
143const DrawerDescription = <T extends ValidComponent = "div">(144 props: DynamicProps<T, DrawerDescriptionProps<T>>,145) => {146 const [, rest] = splitProps(props as DrawerDescriptionProps, ["class"]);147 return (148 <DrawerPrimitive.Description149 data-slot="drawer-description"150 class={cn("text-sm text-muted-foreground", props.class)}151 {...rest}152 />153 );154};155
156export {157 Drawer,158 DrawerPortal,159 DrawerOverlay,160 DrawerTrigger,161 DrawerClose,162 DrawerContent,163 DrawerHeader,164 DrawerFooter,165 DrawerTitle,166 DrawerDescription,167};
Update the import paths to match your project setup.
1import {2 Drawer,3 DrawerClose,4 DrawerContent,5 DrawerDescription,6 DrawerFooter,7 DrawerHeader,8 DrawerTitle,9 DrawerTrigger,10} from "~/components/ui/drawer";
1<Drawer>2 <DrawerTrigger>Open</DrawerTrigger>3 <DrawerContent>4 <DrawerHeader>5 <DrawerTitle>Are you absolutely sure?</DrawerTitle>6 <DrawerDescription>This action cannot be undone.</DrawerDescription>7 </DrawerHeader>8 <DrawerFooter>9 <Button>Submit</Button>10 <DrawerClose>11 <Button variant="outline">Cancel</Button>12 </DrawerClose>13 </DrawerFooter>14 </DrawerContent>15</Drawer>
You can combine the Dialog
and Drawer
components to create a responsive dialog. This renders a Dialog
component on desktop and a Drawer
component on mobile.
1import type { ParentProps } from "solid-js";2
3import { createSignal, Match, Switch } from "solid-js";4
5import { Button } from "~/components/ui/button";6import {7 Dialog,8 DialogContent,9 DialogDescription,10 DialogHeader,11 DialogTitle,12 DialogTrigger,13} from "~/components/ui/dialog";14import {15 Drawer,16 DrawerClose,17 DrawerContent,18 DrawerDescription,19 DrawerFooter,20 DrawerHeader,21 DrawerTitle,22 DrawerTrigger,23} from "~/components/ui/drawer";24import {25 TextField,26 TextFieldInput,27 TextFieldLabel,28} from "~/components/ui/text-field";29import { cn } from "~/lib/utils";30
31export default function DrawerResponsiveDemo() {32 const [open, setOpen] = createSignal(false);33 const isDesktop = window.innerWidth >= 768;34
35 return (36 <Switch>37 <Match when={isDesktop}>38 <Dialog open={open()} onOpenChange={setOpen}>39 <DialogTrigger as={Button<"button">}>Edit Profile</DialogTrigger>40 <DialogContent class="sm:max-w-[425px]">41 <DialogHeader>42 <DialogTitle>Edit profile</DialogTitle>43 <DialogDescription>44 Make changes to your profile here. Click save when you're45 done.46 </DialogDescription>47 </DialogHeader>48 </DialogContent>49 </Dialog>50 </Match>51 <Match when={!isDesktop}>52 <Drawer open={open()} onOpenChange={setOpen}>53 <DrawerTrigger as={Button<"button">}>Edit Profile</DrawerTrigger>54 <DrawerContent>55 <DrawerHeader class="text-left">56 <DrawerTitle>Edit profile</DrawerTitle>57 <DrawerDescription>58 Make changes to your profile here. Click save when you're59 done.60 </DrawerDescription>61 </DrawerHeader>62 <ProfileForm class="px-4" />63 <DrawerFooter class="pt-2">64 <DrawerClose as={Button<"button">} variant="outline">65 Cancel66 </DrawerClose>67 </DrawerFooter>68 </DrawerContent>69 </Drawer>70 </Match>71 </Switch>72 );73}74
75function ProfileForm(props: ParentProps<{ class: string }>) {76 return (77 <form class={cn("grid items-start gap-6", props.class)}>78 <div class="grid gap-3">79 <TextField>80 <TextFieldLabel for="email">Email</TextFieldLabel>81 <TextFieldInput type="email" id="email" value="shadcn@example.com" />82 </TextField>83 </div>84 <div class="grid gap-3">85 <TextField>86 <TextFieldLabel for="name">Name</TextFieldLabel>87 <TextFieldInput type="text" id="name" value="Shad CN" />88 </TextField>89 </div>90 <Button type="submit">Save changes</Button>91 </form>92 );93}