8.0k New Project

Drawer

Previous Next

A drawer component for Svelte.

Docs
<script lang="ts">
  import MinusIcon from "@lucide/svelte/icons/minus";
  import PlusIcon from "@lucide/svelte/icons/plus";
  import * as Drawer from "$lib/components/ui/drawer/index.js";
  import { Button, buttonVariants } from "$lib/components/ui/button/index.js";
  import { BarChart } from "layerchart";
  import { scaleBand } from "d3-scale";
  import { cubicInOut } from "svelte/easing";
 
  const data = [
    {
      goal: 400
    },
    {
      goal: 300
    },
    {
      goal: 200
    },
    {
      goal: 300
    },
    {
      goal: 200
    },
    {
      goal: 278
    },
    {
      goal: 189
    },
    {
      goal: 239
    },
    {
      goal: 300
    },
    {
      goal: 200
    },
    {
      goal: 278
    },
    {
      goal: 189
    },
    {
      goal: 349
    }
  ];
 
  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}
          >
            <MinusIcon />
            <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}
          >
            <PlusIcon />
            <span class="sr-only">Increase</span>
          </Button>
        </div>
        <div class="mt-3 h-[120px]">
          <div class="h-full w-full">
            <BarChart
              data={data.map((d, i) => ({ goal: d.goal, index: i }))}
              y="goal"
              x="index"
              xScale={scaleBand().padding(0.25)}
              axis={false}
              tooltipContext={false}
              props={{
                bars: {
                  stroke: "none",
                  rounded: "all",
                  radius: 4,
                  motion: { type: "tween", duration: 500, easing: cubicInOut },
                  fill: "var(--color-foreground)",
                  fillOpacity: 0.9
                },
                highlight: { area: { fill: "none" } }
              }}
            />
          </div>
        </div>
      </div>
      <Drawer.Footer>
        <Button>Submit</Button>
        <Drawer.Close class={buttonVariants({ variant: "outline" })}
          >Cancel</Drawer.Close
        >
      </Drawer.Footer>
    </div>
  </Drawer.Content>
</Drawer.Root>

About

Drawer is built on top of Vaul Svelte, which is a Svelte port of Vaul by Emil Kowalski.

Installation

pnpm dlx shadcn-svelte@latest add drawer

Usage

<script lang="ts">
  import * as Drawer from "$lib/components/ui/drawer/index.js";
</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>

Examples

Sides

Use the direction prop to set the side of the drawer. Available options are top, right, bottom, and left.

<script lang="ts">
  import * as Drawer from "$lib/components/ui/drawer/index.js";
  import { Button, buttonVariants } from "$lib/components/ui/button/index.js";
  import { cn } from "$lib/utils.js";
 
  const DRAWER_SIDES = ["top", "right", "bottom", "left"] as const;
</script>
 
<div class="flex flex-wrap gap-2">
  {#each DRAWER_SIDES as side (side)}
    <Drawer.Root direction={side === "bottom" ? undefined : side}>
      <Drawer.Trigger
        class={cn(buttonVariants({ variant: "outline" }), "capitalize")}
      >
        {side}
      </Drawer.Trigger>
      <Drawer.Content
        class="data-[vaul-drawer-direction=bottom]:max-h-[50vh] data-[vaul-drawer-direction=top]:max-h-[50vh]"
      >
        <Drawer.Header>
          <Drawer.Title>Move Goal</Drawer.Title>
          <Drawer.Description>Set your daily activity goal.</Drawer.Description>
        </Drawer.Header>
        <div class="no-scrollbar overflow-y-auto px-4">
          {#each Array.from({ length: 10 }) as _, i (i)}
            <p class="mb-4 leading-normal">
              Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
              eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
              enim ad minim veniam, quis nostrud exercitation ullamco laboris
              nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
              reprehenderit in voluptate velit esse cillum dolore eu fugiat
              nulla pariatur. Excepteur sint occaecat cupidatat non proident,
              sunt in culpa qui officia deserunt mollit anim id est laborum.
            </p>
          {/each}
        </div>
        <Drawer.Footer>
          <Button>Submit</Button>
          <Drawer.Close class={buttonVariants({ variant: "outline" })}>
            Cancel
          </Drawer.Close>
        </Drawer.Footer>
      </Drawer.Content>
    </Drawer.Root>
  {/each}
</div>

Responsive Dialog

You can combine the Dialog and Drawer components to create a responsive dialog. This renders a Dialog on desktop and a Drawer on mobile.

<script lang="ts">
  import { MediaQuery } from "svelte/reactivity";
  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)");
 
  const id = $props.id();
</script>
 
{#if isDesktop.current}
  <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-{id}">Email</Label>
          <Input type="email" id="email-{id}" value="shadcn@example.com" />
        </div>
        <div class="grid gap-2">
          <Label for="username-{id}">Username</Label>
          <Input id="username-{id}" 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-start">
        <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-{id}">Email</Label>
          <Input type="email" id="email-{id}" value="shadcn@example.com" />
        </div>
        <div class="grid gap-2">
          <Label for="username-{id}">Username</Label>
          <Input id="username-{id}" 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}