Drawer
A panel that swipes in from an edge, usually the bottom on mobile. Supports a swipe handle, snap points and non-modal use.
import { Button } from "@/components/ui/button";import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@/components/ui/drawer";
export default function Demo() { return ( <Drawer> <DrawerTrigger render={<Button variant="outline" />}> Join MAG4 </DrawerTrigger> <DrawerContent showHandle> <div className="mx-auto w-full max-w-sm"> <DrawerHeader> <DrawerTitle>Join MAG4</DrawerTitle> <DrawerDescription>Amount in USDC</DrawerDescription> </DrawerHeader> <p className="text-center font-mono text-4xl">1,000</p> <DrawerFooter className="flex-row"> <DrawerClose render={<Button variant="outline" className="flex-1" />} > Cancel </DrawerClose> <DrawerClose render={<Button className="flex-1" />}> Join </DrawerClose> </DrawerFooter> </div> </DrawerContent> </Drawer> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@/components/ui/drawer";</script>
<template> <Drawer> <DrawerTrigger as-child> <Button variant="outline">Join MAG4</Button> </DrawerTrigger> <DrawerContent show-handle> <div class="mx-auto w-full max-w-sm"> <DrawerHeader> <DrawerTitle>Join MAG4</DrawerTitle> <DrawerDescription>Amount in USDC</DrawerDescription> </DrawerHeader> <p class="text-center font-mono text-4xl">1,000</p> <DrawerFooter class="flex-row"> <DrawerClose as-child> <Button variant="outline" class="flex-1">Cancel</Button> </DrawerClose> <DrawerClose as-child> <Button class="flex-1">Join</Button> </DrawerClose> </DrawerFooter> </div> </DrawerContent> </Drawer></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import * as Drawer from "$lib/components/ui/drawer";</script>
<Drawer.Root> <Drawer.Trigger> {#snippet child({ props })} <Button variant="outline" {...props}>Join MAG4</Button> {/snippet} </Drawer.Trigger> <Drawer.Content showHandle> <div class="mx-auto w-full max-w-sm"> <Drawer.Header> <Drawer.Title>Join MAG4</Drawer.Title> <Drawer.Description>Amount in USDC</Drawer.Description> </Drawer.Header> <p class="text-center font-mono text-4xl">1,000</p> <Drawer.Footer class="flex-row"> <Drawer.Close> {#snippet child({ props })} <Button variant="outline" class="flex-1" {...props}>Cancel</Button> {/snippet} </Drawer.Close> <Drawer.Close> {#snippet child({ props })} <Button class="flex-1" {...props}>Join</Button> {/snippet} </Drawer.Close> </Drawer.Footer> </div> </Drawer.Content></Drawer.Root>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/drawerpnpm dlx shadcn@latest add @edmi-ui/draweryarn dlx shadcn@latest add @edmi-ui/drawerbunx shadcn@latest add @edmi-ui/drawerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/reactTerminal window pnpm add @base-ui/reactTerminal window yarn add @base-ui/reactTerminal window bun add @base-ui/reactCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/drawerpnpm dlx shadcn-vue@latest add @edmi-ui/draweryarn dlx shadcn-vue@latest add @edmi-ui/drawerbunx shadcn-vue@latest add @edmi-ui/drawerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui @vueuse/coreTerminal window pnpm add reka-ui @vueuse/coreTerminal window yarn add reka-ui @vueuse/coreTerminal window bun add reka-ui @vueuse/coreCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/drawer.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/drawer.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/drawer.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/drawer.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { Button } from "@/components/ui/button";import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@/components/ui/drawer";
<Drawer> <DrawerTrigger render={<Button variant="outline" />}> Join MAG4 </DrawerTrigger> <DrawerContent showHandle> <div className="mx-auto w-full max-w-sm"> <DrawerHeader> <DrawerTitle>Join MAG4</DrawerTitle> <DrawerDescription>Amount in USDC</DrawerDescription> </DrawerHeader> <p className="text-center font-mono text-4xl">1,000</p> <DrawerFooter className="flex-row"> <DrawerClose render={<Button variant="outline" className="flex-1" />} > Cancel </DrawerClose> <DrawerClose render={<Button className="flex-1" />}> Join </DrawerClose> </DrawerFooter> </div> </DrawerContent></Drawer><script setup lang="ts">import { Button } from "@/components/ui/button";import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@/components/ui/drawer";</script>
<template> <Drawer> <DrawerTrigger as-child> <Button variant="outline">Join MAG4</Button> </DrawerTrigger> <DrawerContent show-handle> <div class="mx-auto w-full max-w-sm"> <DrawerHeader> <DrawerTitle>Join MAG4</DrawerTitle> <DrawerDescription>Amount in USDC</DrawerDescription> </DrawerHeader> <p class="text-center font-mono text-4xl">1,000</p> <DrawerFooter class="flex-row"> <DrawerClose as-child> <Button variant="outline" class="flex-1">Cancel</Button> </DrawerClose> <DrawerClose as-child> <Button class="flex-1">Join</Button> </DrawerClose> </DrawerFooter> </div> </DrawerContent> </Drawer></template><script lang="ts"> import * as Drawer from "$lib/components/ui/drawer"; import { Button } from "$lib/components/ui/button";</script>
<Drawer.Root> <Drawer.Trigger> {#snippet child({ props })} <Button variant="outline" {...props}>Join MAG4</Button> {/snippet} </Drawer.Trigger> <!-- … --></Drawer.Root>Component source
Section titled “Component source”import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer";import { cn } from "cn";import type * as React from "react";
function Drawer({ swipeDirection = "down", ...props}: DrawerPrimitive.Root.Props) { return ( <DrawerPrimitive.Root data-slot="drawer" swipeDirection={swipeDirection} {...props} /> );}
function DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) { return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;}
function DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) { return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;}
function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) { return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;}
function DrawerOverlay({ className, ...props}: DrawerPrimitive.Backdrop.Props) { return ( <DrawerPrimitive.Backdrop data-slot="drawer-overlay" className={cn( "fixed inset-0 z-50 min-h-dvh bg-overlay opacity-[calc(1-var(--drawer-swipe-progress,0))] transition-opacity duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 data-[swiping]:duration-0", className, )} {...props} /> );}
function DrawerContent({ className, children, showHandle, ...props}: DrawerPrimitive.Popup.Props & { /** Render a DrawerSwipeHandle at the top (default for the bottom drawer). */ showHandle?: boolean;}) { return ( <DrawerPortal> <DrawerOverlay /> <DrawerPrimitive.Viewport data-slot="drawer-viewport" className="fixed inset-0 z-50" > <DrawerPrimitive.Popup data-slot="drawer-content" className={cn( // Flat: 1px border on the edge facing the page. "group/drawer-content absolute flex flex-col gap-4 overflow-y-auto overscroll-contain border-border bg-popover text-sm text-popover-foreground outline-none transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] data-[swiping]:select-none data-[ending-style]:duration-200", // down = bottom drawer "data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:max-h-[85dvh] data-[swipe-direction=down]:rounded-t-2xl data-[swipe-direction=down]:border-t data-[swipe-direction=down]:[transform:translateY(calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y,0px)))] data-[swipe-direction=down]:data-[ending-style]:translate-y-full data-[swipe-direction=down]:data-[starting-style]:translate-y-full", // up = top drawer "data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:max-h-[85dvh] data-[swipe-direction=up]:rounded-b-2xl data-[swipe-direction=up]:border-b data-[swipe-direction=up]:[transform:translateY(var(--drawer-swipe-movement-y,0px))] data-[swipe-direction=up]:data-[ending-style]:-translate-y-full data-[swipe-direction=up]:data-[starting-style]:-translate-y-full", // left = drawer on the left edge (dismissed by swiping left) "data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:w-3/4 data-[swipe-direction=left]:max-w-sm data-[swipe-direction=left]:rounded-r-2xl data-[swipe-direction=left]:border-r data-[swipe-direction=left]:[transform:translateX(var(--drawer-swipe-movement-x,0px))] data-[swipe-direction=left]:data-[ending-style]:-translate-x-full data-[swipe-direction=left]:data-[starting-style]:-translate-x-full", // right = drawer on the right edge "data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:w-3/4 data-[swipe-direction=right]:max-w-sm data-[swipe-direction=right]:rounded-l-2xl data-[swipe-direction=right]:border-l data-[swipe-direction=right]:[transform:translateX(var(--drawer-swipe-movement-x,0px))] data-[swipe-direction=right]:data-[ending-style]:translate-x-full data-[swipe-direction=right]:data-[starting-style]:translate-x-full", className, )} {...props} > {showHandle && <DrawerSwipeHandle />} <DrawerPrimitive.Content data-slot="drawer-body" className="flex flex-1 flex-col gap-4 p-[22px] pt-0 group-data-[swipe-direction=left]/drawer-content:pt-[22px] group-data-[swipe-direction=right]/drawer-content:pt-[22px]" > {children} </DrawerPrimitive.Content> </DrawerPrimitive.Popup> </DrawerPrimitive.Viewport> </DrawerPortal> );}
/** ✦ Grabber pill shown at the edge of a bottom/top drawer. */function DrawerSwipeHandle({ className, ...props}: React.ComponentProps<"div">) { return ( <div data-slot="drawer-swipe-handle" aria-hidden="true" className={cn( "mx-auto mt-3 h-1.5 w-12 shrink-0 rounded-full bg-border-2", className, )} {...props} /> );}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="drawer-header" className={cn( "flex flex-col gap-1 text-center group-data-[swipe-direction=left]/drawer-content:text-left group-data-[swipe-direction=right]/drawer-content:text-left", className, )} {...props} /> );}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="drawer-footer" className={cn("mt-auto flex flex-col gap-2", className)} {...props} /> );}
function DrawerTitle({ className, ...props }: DrawerPrimitive.Title.Props) { return ( <DrawerPrimitive.Title data-slot="drawer-title" className={cn( "text-base leading-snug font-semibold tracking-[-0.2px] text-foreground", className, )} {...props} /> );}
function DrawerDescription({ className, ...props}: DrawerPrimitive.Description.Props) { return ( <DrawerPrimitive.Description data-slot="drawer-description" className={cn("text-[13px] text-muted-foreground", className)} {...props} /> );}
export { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerSwipeHandle, DrawerTitle, DrawerTrigger,};<script setup lang="ts">import type { DrawerRootEmits, DrawerRootProps } from 'reka-ui'import { DrawerRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<DrawerRootProps>()const emits = defineEmits<DrawerRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)</script>
<template> <DrawerRoot v-slot="slotProps" v-bind="forwarded" > <slot v-bind="slotProps" /> </DrawerRoot></template><script setup lang="ts">import type { DrawerCloseProps } from 'reka-ui'import { DrawerClose } from 'reka-ui'
const props = defineProps<DrawerCloseProps>()</script>
<template> <DrawerClose data-slot="drawer-close" v-bind="props" > <slot /> </DrawerClose></template><script setup lang="ts">import type { DrawerContentEmits, DrawerContentProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { DrawerContent, DrawerPortal, useForwardPropsEmits,} from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'import DrawerOverlay from './DrawerOverlay.vue'import DrawerSwipeHandle from './DrawerSwipeHandle.vue'
defineOptions({ inheritAttrs: false,})
const props = defineProps<DrawerContentProps & { class?: HTMLAttributes['class'] /** Render a DrawerSwipeHandle at the top (default for the bottom drawer). */ showHandle?: boolean}>()const emits = defineEmits<DrawerContentEmits>()
const delegatedProps = reactiveOmit(props, 'class', 'showHandle')
const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <DrawerPortal> <DrawerOverlay /> <DrawerContent data-slot="drawer-content" v-bind="{ ...$attrs, ...forwarded }" :class="cn( // Flat: 1px border on the edge facing the page. 'group/drawer-content fixed z-50 flex flex-col gap-4 overflow-y-auto overscroll-contain border-border bg-popover text-sm text-popover-foreground outline-none will-change-transform transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] data-[swiping]:select-none data-[swiping]:duration-0 data-[state=open]:animate-in data-[state=closed]:animate-out', // down = bottom drawer 'data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:max-h-[85dvh] data-[swipe-direction=down]:rounded-t-2xl data-[swipe-direction=down]:border-t data-[swipe-direction=down]:[transform:translateY(calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y,0px)))] data-[swipe-direction=down]:data-[state=open]:slide-in-from-bottom data-[swipe-direction=down]:data-[state=closed]:slide-out-to-bottom', // up = top drawer 'data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:max-h-[85dvh] data-[swipe-direction=up]:rounded-b-2xl data-[swipe-direction=up]:border-b data-[swipe-direction=up]:[transform:translateY(var(--drawer-swipe-movement-y,0px))] data-[swipe-direction=up]:data-[state=open]:slide-in-from-top data-[swipe-direction=up]:data-[state=closed]:slide-out-to-top', // left = drawer on the left edge (dismissed by swiping left) 'data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:w-3/4 data-[swipe-direction=left]:max-w-sm data-[swipe-direction=left]:rounded-r-2xl data-[swipe-direction=left]:border-r data-[swipe-direction=left]:[transform:translateX(var(--drawer-swipe-movement-x,0px))] data-[swipe-direction=left]:data-[state=open]:slide-in-from-left data-[swipe-direction=left]:data-[state=closed]:slide-out-to-left', // right = drawer on the right edge 'data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:w-3/4 data-[swipe-direction=right]:max-w-sm data-[swipe-direction=right]:rounded-l-2xl data-[swipe-direction=right]:border-l data-[swipe-direction=right]:[transform:translateX(var(--drawer-swipe-movement-x,0px))] data-[swipe-direction=right]:data-[state=open]:slide-in-from-right data-[swipe-direction=right]:data-[state=closed]:slide-out-to-right', props.class, )" > <DrawerSwipeHandle v-if="showHandle" /> <div data-slot="drawer-body" class="flex flex-1 flex-col gap-4 p-[22px] pt-0 group-data-[swipe-direction=left]/drawer-content:pt-[22px] group-data-[swipe-direction=right]/drawer-content:pt-[22px]" > <slot /> </div> </DrawerContent> </DrawerPortal></template><script setup lang="ts">import type { DrawerDescriptionProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { DrawerDescription } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<DrawerDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')</script>
<template> <DrawerDescription data-slot="drawer-description" v-bind="delegatedProps" :class="cn('text-[13px] text-muted-foreground', props.class)" > <slot /> </DrawerDescription></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="drawer-footer" :class="cn('mt-auto flex flex-col gap-2', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="drawer-header" :class="cn('flex flex-col gap-1 text-center group-data-[swipe-direction=left]/drawer-content:text-left group-data-[swipe-direction=right]/drawer-content:text-left', props.class)" > <slot /> </div></template><script setup lang="ts">import type { DrawerOverlayProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { DrawerOverlay } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<DrawerOverlayProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')</script>
<template> <DrawerOverlay data-slot="drawer-overlay" v-bind="delegatedProps" :class="cn('fixed inset-0 z-50 min-h-dvh bg-overlay opacity-[calc(1-var(--drawer-swipe-progress,0))] duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[swiping]:duration-0', props.class)" /></template><script setup lang="ts">import type { DrawerHandleProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { DrawerHandle, useForwardProps } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
// ✦ Grabber pill shown at the edge of a bottom/top drawer.const props = defineProps<DrawerHandleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)</script>
<template> <DrawerHandle data-slot="drawer-swipe-handle" v-bind="forwardedProps" :class="cn('mx-auto mt-3 h-1.5 w-12 shrink-0 rounded-full bg-border-2', props.class)" > <slot /> </DrawerHandle></template><script setup lang="ts">import type { DrawerTitleProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { DrawerTitle } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<DrawerTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')</script>
<template> <DrawerTitle data-slot="drawer-title" v-bind="delegatedProps" :class="cn('text-base leading-snug font-semibold tracking-[-0.2px] text-foreground', props.class)" > <slot /> </DrawerTitle></template><script setup lang="ts">import type { DrawerTriggerProps } from 'reka-ui'import { DrawerTrigger } from 'reka-ui'
const props = defineProps<DrawerTriggerProps>()</script>
<template> <DrawerTrigger data-slot="drawer-trigger" v-bind="props" > <slot /> </DrawerTrigger></template>export { default as Drawer } from "./Drawer.vue";export { default as DrawerClose } from "./DrawerClose.vue";export { default as DrawerContent } from "./DrawerContent.vue";export { default as DrawerDescription } from "./DrawerDescription.vue";export { default as DrawerFooter } from "./DrawerFooter.vue";export { default as DrawerHeader } from "./DrawerHeader.vue";export { default as DrawerOverlay } from "./DrawerOverlay.vue";export { default as DrawerSwipeHandle } from "./DrawerSwipeHandle.vue";export { default as DrawerTitle } from "./DrawerTitle.vue";export { default as DrawerTrigger } from "./DrawerTrigger.vue";<script lang="ts"> import { Drawer as Primitive } from "vaul-svelte";
let { ref = $bindable(null), ...restProps }: Primitive.CloseProps = $props();</script>
<Primitive.Close bind:ref data-slot="drawer-close" {...restProps} /><script lang="ts"> import { Drawer as DrawerPrimitive } from "vaul-svelte"; import { cn } from "$lib/utils.js"; import type { WithoutChildrenOrChild } from "$lib/utils.js"; import DrawerOverlay from "./drawer-overlay.svelte"; import DrawerPortal from "./drawer-portal.svelte"; import DrawerSwipeHandle from "./drawer-swipe-handle.svelte"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, portalProps, showHandle, children, ...restProps }: DrawerPrimitive.ContentProps & { portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DrawerPortal>>; /** Render a DrawerSwipeHandle at the top (default for the bottom drawer). */ showHandle?: boolean; } = $props();</script>
<DrawerPortal {...portalProps}> <DrawerOverlay /> <DrawerPrimitive.Content bind:ref data-slot="drawer-content" class={cn( // Flat: a 1px border on the edge facing the page. "group/drawer-content fixed z-50 flex h-auto flex-col gap-4 overflow-y-auto overscroll-contain border-border bg-popover text-sm text-popover-foreground outline-none", "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[85dvh] data-[vaul-drawer-direction=bottom]:rounded-t-2xl data-[vaul-drawer-direction=bottom]:border-t", "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[85dvh] data-[vaul-drawer-direction=top]:rounded-b-2xl data-[vaul-drawer-direction=top]:border-b", "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:max-w-sm data-[vaul-drawer-direction=left]:rounded-r-2xl data-[vaul-drawer-direction=left]:border-r", "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:max-w-sm data-[vaul-drawer-direction=right]:rounded-l-2xl data-[vaul-drawer-direction=right]:border-l", className )} {...restProps} > {#if showHandle} <DrawerSwipeHandle /> {/if} <div data-slot="drawer-body" class="flex flex-1 flex-col gap-4 p-[22px] pt-0 group-data-[vaul-drawer-direction=left]/drawer-content:pt-[22px] group-data-[vaul-drawer-direction=right]/drawer-content:pt-[22px]" > {@render children?.()} </div> </DrawerPrimitive.Content></DrawerPortal><script lang="ts"> import { Drawer as Primitive } from "vaul-svelte"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: Primitive.DescriptionProps = $props();</script>
<Primitive.Description bind:ref data-slot="drawer-description" class={cn("text-[13px] text-muted-foreground", className)} {...restProps}/><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="drawer-footer" class={cn("mt-auto flex flex-col gap-2", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="drawer-header" class={cn("flex flex-col gap-1 text-center group-data-[vaul-drawer-direction=left]/drawer-content:text-left group-data-[vaul-drawer-direction=right]/drawer-content:text-left", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { Drawer as DrawerPrimitive } from "vaul-svelte";
let { shouldScaleBackground = true, open = $bindable(false), activeSnapPoint = $bindable(null), ...restProps }: DrawerPrimitive.RootProps = $props();</script>
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} /><script lang="ts"> import { Drawer as Primitive } from "vaul-svelte"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: Primitive.OverlayProps = $props();</script>
<Primitive.Overlay bind:ref data-slot="drawer-overlay" class={cn("fixed inset-0 z-50 bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", className)} {...restProps}/><script lang="ts"> import { Drawer as Primitive } from "vaul-svelte";
let { ...restProps }: Primitive.PortalProps = $props();</script>
<Primitive.Portal {...restProps} /><script lang="ts"> import { Drawer as Primitive } from "vaul-svelte"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: Primitive.HandleProps = $props();</script>
<Primitive.Handle bind:ref data-slot="drawer-swipe-handle" class={cn("mx-auto mt-3 h-1.5 w-12 shrink-0 rounded-full bg-border-2", className)} {...restProps}/><script lang="ts"> import { Drawer as Primitive } from "vaul-svelte"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: Primitive.TitleProps = $props();</script>
<Primitive.Title bind:ref data-slot="drawer-title" class={cn("text-base leading-snug font-semibold tracking-[-0.2px] text-foreground", className)} {...restProps}/><script lang="ts"> import { Drawer as Primitive } from "vaul-svelte";
let { ref = $bindable(null), ...restProps }: Primitive.TriggerProps = $props();</script>
<Primitive.Trigger bind:ref data-slot="drawer-trigger" {...restProps} /><script lang="ts"> import { Drawer as DrawerPrimitive } from "vaul-svelte";
// Edmi API parity with the React/Vue drawer: `swipeDirection` is the dismiss direction // (down = bottom drawer). vaul-svelte names the edge instead (`direction`); either works. type SwipeDirection = "up" | "down" | "left" | "right"; const edgeOf: Record<SwipeDirection, "top" | "bottom" | "left" | "right"> = { up: "top", down: "bottom", left: "left", right: "right", };
let { shouldScaleBackground = true, open = $bindable(false), activeSnapPoint = $bindable(null), swipeDirection, direction, ...restProps }: DrawerPrimitive.RootProps & { swipeDirection?: SwipeDirection } = $props();
const edge = $derived(direction ?? (swipeDirection ? edgeOf[swipeDirection] : undefined));</script>
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint direction={edge} {...restProps}/>import Root from "./drawer.svelte";import Close from "./drawer-close.svelte";import Content from "./drawer-content.svelte";import Description from "./drawer-description.svelte";import Footer from "./drawer-footer.svelte";import Header from "./drawer-header.svelte";import NestedRoot from "./drawer-nested.svelte";import Overlay from "./drawer-overlay.svelte";import Portal from "./drawer-portal.svelte";import SwipeHandle from "./drawer-swipe-handle.svelte";import Title from "./drawer-title.svelte";import Trigger from "./drawer-trigger.svelte";
export { Close, Close as DrawerClose, Content, Content as DrawerContent, Description, Description as DrawerDescription, Footer, Footer as DrawerFooter, Header, Header as DrawerHeader, NestedRoot, NestedRoot as DrawerNestedRoot, Overlay, Overlay as DrawerOverlay, Portal, Portal as DrawerPortal, Root, // Root as Drawer, SwipeHandle, SwipeHandle as DrawerSwipeHandle, Title, Title as DrawerTitle, Trigger, Trigger as DrawerTrigger,};| Prop | Type | Default |
|---|---|---|
Drawer swipeDirection |
down | up | left | right |
down |
Drawer snapPoints |
(number | string)[] (0-1 = viewport fraction, "148px", "30rem") |
none |
Drawer snapPoint / onSnapPointChange |
controlled snap point | uncontrolled |
Drawer modal |
boolean | "trap-focus" |
true |
DrawerContent showHandle |
boolean |
false |
Parts: Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerFooter, DrawerClose, DrawerSwipeHandle. Built on the Base UI Drawer (not vaul).
✦ Edmi additions
Section titled “✦ Edmi additions”DrawerSwipeHandlegrabber andshowHandle; hard 4px strong lip, no blur.
Vue / Svelte differences
Section titled “Vue / Svelte differences”- Svelte (vaul-svelte) names the edge in
direction(top | bottom | left | right). Edmi also acceptsswipeDirectionfor parity with React and Vue and maps it:up->top,down->bottom,left->left,right->right.directionwins if both are set.