A drawer component for Svelte.
<script lang="ts"> import Minus from "svelte-radix/Minus.svelte"; import Plus from "svelte-radix/Plus.svelte"; import { VisGroupedBar, VisXYContainer } from "@unovis/svelte"; import { Button, buttonVariants } from "$lib/components/ui/button/index.js"; import * as Drawer from "$lib/components/ui/drawer/index.js"; const data = [ { id: 1, goal: 400 }, { id: 2, goal: 300 }, { id: 3, goal: 200 }, { id: 4, goal: 300 }, { id: 5, goal: 200 }, { id: 6, goal: 278 }, { id: 7, goal: 189 }, { id: 8, goal: 239 }, { id: 9, goal: 300 }, { id: 10, goal: 200 }, { id: 11, goal: 278 }, { id: 12, goal: 189 }, { id: 13, goal: 349 } ]; const x = (d: { goal: number; id: number }) => d.id; const y = (d: { goal: number; id: number }) => d.goal; let goal = 350; function handleClick(adjustment: number) { goal = Math.max(200, Math.min(400, goal + adjustment)); } </script> <Drawer.Root> <Drawer.Trigger class={buttonVariants({ variant: "outline" })} >Open Drawer</Drawer.Trigger > <Drawer.Content> <div class="mx-auto w-full max-w-sm"> <Drawer.Header> <Drawer.Title>Move Goal</Drawer.Title> <Drawer.Description>Set your daily activity goal.</Drawer.Description> </Drawer.Header> <div class="p-4 pb-0"> <div class="flex items-center justify-center space-x-2"> <Button variant="outline" size="icon" class="size-8 shrink-0 rounded-full" onclick={() => handleClick(-10)} disabled={goal <= 200} > <Minus class="size-4" /> <span class="sr-only">Decrease</span> </Button> <div class="flex-1 text-center"> <div class="text-7xl font-bold tracking-tighter"> {goal} </div> <div class="text-muted-foreground text-[0.70rem] uppercase"> Calories/day </div> </div> <Button variant="outline" size="icon" class="size-8 shrink-0 rounded-full" onclick={() => handleClick(10)} > <Plus class="size-4" /> <span class="sr-only">Increase</span> </Button> </div> <div class="mt-3 h-[120px]"> <VisXYContainer {data} height={60}> <VisGroupedBar {x} {y} color="hsl(var(--primary) / 0.2)" /> </VisXYContainer> </div> </div> <Drawer.Footer> <Button>Submit</Button> <Drawer.Close class={buttonVariants({ variant: "outline" })} >Cancel</Drawer.Close > </Drawer.Footer> </div> </Drawer.Content> </Drawer.Root>
<script lang="ts"> import Minus from "lucide-svelte/icons/minus"; import Plus from "lucide-svelte/icons/plus"; import { VisGroupedBar, VisXYContainer } from "@unovis/svelte"; import * as Drawer from "$lib/components/ui/drawer/index.js"; import { Button, buttonVariants } from "$lib/components/ui/button/index.js"; const data = [ { id: 1, goal: 400 }, { id: 2, goal: 300 }, { id: 3, goal: 200 }, { id: 4, goal: 300 }, { id: 5, goal: 200 }, { id: 6, goal: 278 }, { id: 7, goal: 189 }, { id: 8, goal: 239 }, { id: 9, goal: 300 }, { id: 10, goal: 200 }, { id: 11, goal: 278 }, { id: 12, goal: 189 }, { id: 13, goal: 349 } ]; const x = (d: { goal: number; id: number }) => d.id; const y = (d: { goal: number; id: number }) => d.goal; let goal = $state(350); function handleClick(adjustment: number) { goal = Math.max(200, Math.min(400, goal + adjustment)); } </script> <Drawer.Root> <Drawer.Trigger class={buttonVariants({ variant: "outline" })} >Open Drawer</Drawer.Trigger > <Drawer.Content> <div class="mx-auto w-full max-w-sm"> <Drawer.Header> <Drawer.Title>Move Goal</Drawer.Title> <Drawer.Description>Set your daily activity goal.</Drawer.Description> </Drawer.Header> <div class="p-4 pb-0"> <div class="flex items-center justify-center space-x-2"> <Button variant="outline" size="icon" class="size-8 shrink-0 rounded-full" onclick={() => handleClick(-10)} disabled={goal <= 200} > <Minus class="size-4" /> <span class="sr-only">Decrease</span> </Button> <div class="flex-1 text-center"> <div class="text-7xl font-bold tracking-tighter"> {goal} </div> <div class="text-muted-foreground text-[0.70rem] uppercase"> Calories/day </div> </div> <Button variant="outline" size="icon" class="size-8 shrink-0 rounded-full" onclick={() => handleClick(10)} disabled={goal >= 400} > <Plus class="size-4" /> <span class="sr-only">Increase</span> </Button> </div> <div class="mt-3 h-[120px]"> <VisXYContainer {data} height={60}> <VisGroupedBar {x} {y} color="hsl(var(--primary) / 0.2)" /> </VisXYContainer> </div> </div> <Drawer.Footer> <Button>Submit</Button> <Drawer.Close class={buttonVariants({ variant: "outline" })} >Cancel</Drawer.Close > </Drawer.Footer> </div> </Drawer.Content> </Drawer.Root>
Drawer is built on top of Vaul Svelte, which is a Svelte port of Vaul by Emil Kowalski.
npx shadcn-svelte@latest add drawer
Install vaul-svelte:
vaul-svelte
npm install vaul-svelte -D
<script lang="ts"> import * as Drawer from "$lib/components/ui/drawer"; </script> <Drawer.Root> <Drawer.Trigger>Open</Drawer.Trigger> <Drawer.Content> <Drawer.Header> <Drawer.Title>Are you sure absolutely sure?</Drawer.Title> <Drawer.Description>This action cannot be undone.</Drawer.Description> </Drawer.Header> <Drawer.Footer> <Button>Submit</Button> <Drawer.Close>Cancel</Drawer.Close> </Drawer.Footer> </Drawer.Content> </Drawer.Root>
You can combine the Dialog and Drawer components to create a responsive dialog. This renders a Dialog on desktop and a Drawer on mobile.
Dialog
Drawer
<script lang="ts"> import { MediaQuery } from "runed"; import * as Dialog from "$lib/components/ui/dialog/index.js"; import * as Drawer from "$lib/components/ui/drawer/index.js"; import { Input } from "$lib/components/ui/input/index.js"; import { Label } from "$lib/components/ui/label/index.js"; import { Button, buttonVariants } from "$lib/components/ui/button/index.js"; let open = $state(false); const isDesktop = new MediaQuery("(min-width: 768px)"); </script> {#if isDesktop.matches} <Dialog.Root bind:open> <Dialog.Trigger class={buttonVariants({ variant: "outline" })} >Edit Profile</Dialog.Trigger > <Dialog.Content class="sm:max-w-[425px]"> <Dialog.Header> <Dialog.Title>Edit profile</Dialog.Title> <Dialog.Description> Make changes to your profile here. Click save when you're done. </Dialog.Description> </Dialog.Header> <form class="grid items-start gap-4"> <div class="grid gap-2"> <Label for="email">Email</Label> <Input type="email" id="email" value="shadcn@example.com" /> </div> <div class="grid gap-2"> <Label for="username">Username</Label> <Input id="username" value="@shadcn" /> </div> <Button type="submit">Save changes</Button> </form> </Dialog.Content> </Dialog.Root> {:else} <Drawer.Root bind:open> <Drawer.Trigger class={buttonVariants({ variant: "outline" })} >Edit Profile</Drawer.Trigger > <Drawer.Content> <Drawer.Header class="text-left"> <Drawer.Title>Edit profile</Drawer.Title> <Drawer.Description> Make changes to your profile here. Click save when you're done. </Drawer.Description> </Drawer.Header> <form class="grid items-start gap-4 px-4"> <div class="grid gap-2"> <Label for="email">Email</Label> <Input type="email" id="email" value="shadcn@example.com" /> </div> <div class="grid gap-2"> <Label for="username">Username</Label> <Input id="username" value="@shadcn" /> </div> <Button type="submit">Save changes</Button> </form> <Drawer.Footer class="pt-2"> <Drawer.Close class={buttonVariants({ variant: "outline" })} >Cancel</Drawer.Close > </Drawer.Footer> </Drawer.Content> </Drawer.Root> {/if}
<script lang="ts"> import { MediaQuery } from "runed"; import * as Dialog from "$lib/components/ui/dialog/index.js"; import * as Drawer from "$lib/components/ui/drawer/index.js"; import { Input } from "$lib/components/ui/input/index.js"; import { Label } from "$lib/components/ui/label/index.js"; import { Button, buttonVariants } from "$lib/components/ui/button/index.js"; let open = false; const isDesktop = new MediaQuery("(min-width: 768px)"); </script> {#if isDesktop.matches} <Dialog.Root bind:open> <Dialog.Trigger class={buttonVariants({ variant: "outline" })} >Edit Profile</Dialog.Trigger > <Dialog.Content class="sm:max-w-[425px]"> <Dialog.Header> <Dialog.Title>Edit profile</Dialog.Title> <Dialog.Description> Make changes to your profile here. Click save when you're done. </Dialog.Description> </Dialog.Header> <form class="grid items-start gap-4"> <div class="grid gap-2"> <Label for="email">Email</Label> <Input type="email" id="email" value="shadcn@example.com" /> </div> <div class="grid gap-2"> <Label for="username">Username</Label> <Input id="username" value="@shadcn" /> </div> <Button type="submit">Save changes</Button> </form> </Dialog.Content> </Dialog.Root> {:else} <Drawer.Root bind:open> <Drawer.Trigger class={buttonVariants({ variant: "outline" })} >Edit Profile</Drawer.Trigger > <Drawer.Content> <Drawer.Header class="text-left"> <Drawer.Title>Edit profile</Drawer.Title> <Drawer.Description> Make changes to your profile here. Click save when you're done. </Drawer.Description> </Drawer.Header> <form class="grid items-start gap-4 px-4"> <div class="grid gap-2"> <Label for="email">Email</Label> <Input type="email" id="email" value="shadcn@example.com" /> </div> <div class="grid gap-2"> <Label for="username">Username</Label> <Input id="username" value="@shadcn" /> </div> <Button type="submit">Save changes</Button> </form> <Drawer.Footer class="pt-2"> <Drawer.Close class={buttonVariants({ variant: "outline" })} >Cancel</Drawer.Close > </Drawer.Footer> </Drawer.Content> </Drawer.Root> {/if}
On This Page