<script setup>
import { ref } from "vue";
import { Button } from "@dicehub/kappa/components/button";
import { Drawer } from "@dicehub/kappa/components/drawer";
const goal = ref(350);
const adjustGoal = (amount) => {
goal.value = Math.min(600, Math.max(200, goal.value + amount));
};
</script>
<template>
<Drawer.Root swipe-direction="end">
<Drawer.Trigger as-child><Button variant="outline">Open Drawer</Button></Drawer.Trigger>
<Drawer.Content show-close-button>
<Drawer.Header>
<Drawer.Title>Move Goal</Drawer.Title>
<Drawer.Description>Set your daily activity goal.</Drawer.Description>
</Drawer.Header>
<div class="goal" data-no-drag>
<Button aria-label="Decrease goal" @click="adjustGoal(-10)">−</Button>
<output>{{ goal }} Calories/day</output>
<Button aria-label="Increase goal" @click="adjustGoal(10)">+</Button>
</div>
<Drawer.Footer>
<Drawer.Close as-child><Button variant="primary">Submit</Button></Drawer.Close>
<Drawer.Close as-child><Button variant="secondary">Cancel</Button></Drawer.Close>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>
</template>Installation
Barrel
import {
Drawer,
DrawerRoot,
DrawerContent,
DrawerTitle,
DrawerDescription,
DrawerClose,
type DrawerProps,
type DrawerSnapPoint,
type DrawerSwipeDirection,
} from "@dicehub/kappa";Granular
import {
Drawer,
DrawerRoot,
DrawerContent,
DrawerTitle,
DrawerDescription,
DrawerClose,
type DrawerProps,
type DrawerSnapPoint,
type DrawerSwipeDirection,
} from "@dicehub/kappa/components/drawer";Usage
Put Trigger and Content inside Drawer.Root.Content supplies the portal layers and drag handle. Add a visibleClose action in the footer, or enable the built-in close button.
<script setup>
import { Button } from "@dicehub/kappa/components/button";
import { Drawer } from "@dicehub/kappa/components/drawer";
</script>
<template>
<Drawer.Root>
<Drawer.Trigger as-child><Button>Open drawer</Button></Drawer.Trigger>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Are you absolutely sure?</Drawer.Title>
<Drawer.Description>This action cannot be undone.</Drawer.Description>
</Drawer.Header>
<Drawer.Footer>
<Button variant="primary">Submit</Button>
<Drawer.Close as-child><Button variant="secondary">Cancel</Button></Drawer.Close>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>
</template>Composition
Drawer.Root
├── Drawer.Trigger
└── Drawer.Content
├── Drawer.Grabber
│ └── Drawer.GrabberIndicator
├── Drawer.Header
│ ├── Drawer.Title
│ └── Drawer.Description
├── Content
├── Drawer.Footer
└── Drawer.CloseArk UI Drawer owns open state, gestures, snap points, modality, focus trapping, dismissal, aria relationships, presence, trigger values, and nested stacks. Kappa owns the compound API, composed portal surface, logical edge geometry, semantic tokens, and motion.
DrawerandDrawer.Rootare the same root component.Contentteleports tobody. Pass:teleport="false"for an in-place test or constrained integration.Backdrop,Positioner,Grabber, andGrabberIndicatorremain public for lower-level compositions.- Use
as-childon Trigger and Close when a Kappa Button owns the visual element. - Use
Drawer.Stackaround nested roots. Indent parts are available when the application page must react to the stack.
<script setup>
import {
DrawerRoot,
DrawerTrigger,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerDescription,
DrawerFooter,
DrawerClose,
} from "@dicehub/kappa/components/drawer";
</script>
<template>
<DrawerRoot>
<DrawerTrigger>Open</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Title</DrawerTitle>
<DrawerDescription>Description</DrawerDescription>
</DrawerHeader>
<DrawerFooter><DrawerClose>Close</DrawerClose></DrawerFooter>
</DrawerContent>
</DrawerRoot>
</template>Reference Differences
Kappa supports sizing, position, handle, nesting, non-modal use, snap points, and responsive Dialog composition. Kappa uses Ark UI's logical start and end directions instead of physicalleft and right props.
Sizing and Gestures
- A vertical drawer sizes to content and is capped by the dynamic viewport. Set
block-sizefor a fixed or proportional panel. - A side drawer defaults to 24rem. Override
--kappa-drawer-inline-size; the three-rem viewport remainder remains. - The full content surface is draggable by default. Set
:draggable="false"to limit dragging to Grabber. - Add
data-no-dragto fields, charts, editors, and custom scroll regions that must own pointer gestures. - Snap points support viewport fractions, pixel numbers, and CSS lengths. Use them only for vertical drawers.
- SwipeArea can open a closed drawer from the viewport edge. Keep that invisible gesture area narrow so it does not block page controls.
Examples
Basic
A complete drawer has a clear title, concise supporting text, focused content, and explicit actions. Content supplies the portal, backdrop, positioner, surface, and grabber.
<Drawer.Root>
<Drawer.Trigger as-child><Button>Open drawer</Button></Drawer.Trigger>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Edit profile</Drawer.Title>
<Drawer.Description>Make changes to your profile here. Save when you are done.</Drawer.Description>
</Drawer.Header>
<form data-no-drag><!-- Profile fields --></form>
<Drawer.Footer>
<Drawer.Close as-child><Button>Save changes</Button></Drawer.Close>
<Drawer.Close as-child><Button variant="secondary">Cancel</Button></Drawer.Close>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>Positions
Render the same content from each supported edge. The LTR controls use physical labels; the API uses logical start and end values so side drawers adapt to text direction.
<Drawer.Root swipe-direction="up"><!-- Move Goal --></Drawer.Root>
<Drawer.Root swipe-direction="end"><!-- Move Goal --></Drawer.Root>
<Drawer.Root swipe-direction="down"><!-- Move Goal --></Drawer.Root>
<Drawer.Root swipe-direction="start"><!-- Move Goal --></Drawer.Root>Custom Sizes
Set block size on vertical content or --kappa-drawer-inline-size on side content. The built-in viewport caps remain active.
<Drawer.Content style="block-size: 50dvb">...</Drawer.Content>
<Drawer.Root swipe-direction="end">
<Drawer.Content style="--kappa-drawer-inline-size: 30rem">...</Drawer.Content>
</Drawer.Root>Swipe Handle
The grabber follows the active edge. The same header and content hierarchy works in all four directions.
<Drawer.Root swipe-direction="up"><Drawer.Content show-grabber>...</Drawer.Content></Drawer.Root>
<Drawer.Root swipe-direction="end"><Drawer.Content show-grabber>...</Drawer.Content></Drawer.Root>
<Drawer.Root swipe-direction="down"><Drawer.Content show-grabber>...</Drawer.Content></Drawer.Root>
<Drawer.Root swipe-direction="start"><Drawer.Content show-grabber>...</Drawer.Content></Drawer.Root>Snap Points
Vertical drawers can stop at fractional, pixel, or CSS-length positions. Bind snapPoint when the application needs the active value.
<script setup>
import { ref } from "vue";
const snapPoint = ref(0.35);
</script>
<Drawer.Root
v-model:snap-point="snapPoint"
:snap-points="[0.35, 0.65, 1]"
snap-to-sequential-points
>
<Drawer.Content style="block-size: min(42rem, calc(100dvb - 3rem))">...</Drawer.Content>
</Drawer.Root>Scrollable Content
Keep Header and Footer fixed. Make the middle flex child scrollable and add data-no-drag so scrolling does not start a drawer gesture.
<Drawer.Content class="policy-drawer">
<Drawer.Header>...</Drawer.Header>
<div class="policy-drawer__body" data-no-drag tabindex="0">...</div>
<Drawer.Footer>...</Drawer.Footer>
</Drawer.Content>
<style scoped>
.policy-drawer { block-size: min(34rem, calc(100dvb - 3rem)); }
.policy-drawer__body { min-block-size: 0; flex: 1; overflow-y: auto; }
</style>Handle-only Drag
Set draggable to false when users must select or manipulate content. The Grabber remains draggable.
<Drawer.Content :draggable="false">
<Drawer.Header>
<Drawer.Title>Copy confirmation code</Drawer.Title>
</Drawer.Header>
<p>Select the code without moving the panel.</p>
</Drawer.Content>Non-modal
Disable modal focus and scroll restrictions when the panel supplements an active page. Remove the backdrop and choose outside-dismissal behavior explicitly.
<Drawer.Root
:close-on-interact-outside="false"
:modal="false"
:prevent-scroll="false"
:trap-focus="false"
swipe-direction="end"
>
<Drawer.Content :show-backdrop="false">...</Drawer.Content>
</Drawer.Root>Controlled
Bind v-model:open when application state owns visibility. Keep Root mounted so exit motion and focus restoration can finish.
<script setup>
import { ref } from "vue";
const open = ref(false);
</script>
<Button @click="open = true">Open controlled drawer</Button>
<Drawer.Root v-model:open="open">
<Drawer.Content>
<Drawer.Title>Export account data</Drawer.Title>
<Drawer.Footer><Button @click="open = false">Export</Button></Drawer.Footer>
</Drawer.Content>
</Drawer.Root>Multiple Triggers
Give each Trigger a value and bind triggerValue when one Drawer surface presents context for several source controls.
<Drawer.Root v-model:trigger-value="activeSetting">
<Drawer.Trigger as-child value="profile"><Button>Profile</Button></Drawer.Trigger>
<Drawer.Trigger as-child value="security"><Button>Security</Button></Drawer.Trigger>
<Drawer.Content>...</Drawer.Content>
</Drawer.Root>Nested Drawers
Wrap related roots in Drawer.Stack. A child drawer opens above its parent while Ark UI coordinates layer state and Kappa scales the parent surface.
<Drawer.Stack>
<Drawer.Root>
<Drawer.Trigger>Open settings</Drawer.Trigger>
<Drawer.Content>
<Drawer.Root>
<Drawer.Trigger>Advanced settings</Drawer.Trigger>
<Drawer.Content>...</Drawer.Content>
</Drawer.Root>
</Drawer.Content>
</Drawer.Root>
</Drawer.Stack>Responsive Dialog
Use Dialog for wide screens and Drawer for compact screens. Preserve the same title, fields, actions, and controlled state across both branches.
<Dialog.Root v-if="!isCompact">...</Dialog.Root>
<Drawer.Root v-else>...</Drawer.Root>Right-to-left
The logical start and end directions follow the inherited locale. In RTL, start resolves to the right edge.
<Drawer.Root locale="ar" swipe-direction="start">
<Drawer.Content dir="rtl">
<Drawer.Title>ملخص الحساب</Drawer.Title>
<Drawer.Description>تفتح اللوحة من البداية المنطقية.</Drawer.Description>
</Drawer.Content>
</Drawer.Root>Accessibility
- Render a visible
Titleand normally aDescription. They label and describe the dialog surface. - Tab and Shift+Tab stay inside a modal drawer. Escape closes it when enabled. Focus then returns to the opening trigger.
- Do not depend on swiping. Always provide a keyboard-operable Close action.
- Keep controls at least clear and reachable near the handle. The Grabber is a pointer target, not the only dismissal control.
- Use non-modal behavior only when users must continue to interact with the page. Disable the backdrop and focus trap together.
- Scrollable regions need a visible focus indicator and
tabindex="0"when keyboard users must scroll them independently. - Logical start and end positions support RTL. Open and close motion stops under reduced-motion preferences.
API Reference
Drawer.Root
| Prop | Type | Default | Description |
|---|---|---|---|
open / defaultOpen | boolean | false | Controls or initializes visibility. |
swipeDirection | "up" | "down" | "start" | "end" | "down" | Sets the edge and dismiss gesture. Start and end are logical directions. |
snapPoints | (number | string)[] | [1] | Defines available vertical resting positions. |
snapPoint / defaultSnapPoint | number | string | null | 1 | Controls or initializes the active snap point. |
snapToSequentialPoints | boolean | false | Moves through adjacent snap points instead of selecting by release position. |
closeThreshold | number | 0.25 | Sets the fractional drag distance that dismisses the drawer. |
swipeVelocityThreshold | number | 700 | Sets the velocity in pixels per second that dismisses the drawer. |
preventDragOnScroll | boolean | true | Prevents a gesture from starting on a scrollable element. |
closeOnEscape / closeOnInteractOutside | boolean | true | Controls keyboard and outside-interaction dismissal. |
modal | boolean | true | Blocks pointer and assistive technology access outside the drawer. |
trapFocus | boolean | true | Keeps keyboard focus inside while open. |
preventScroll | boolean | true | Prevents page scrolling behind a modal drawer. |
restoreFocus | boolean | true | Returns focus to the opening trigger or finalFocusEl. |
initialFocusEl / finalFocusEl | () => HTMLElement | null | - | Overrides initial and restored focus targets. |
role | "dialog" | "alertdialog" | "dialog" | Sets the accessible layer role. |
locale | string | "en-US" | Sets the Ark UI locale used to resolve logical start and end directions. |
lazyMount / unmountOnExit | boolean | true | Defers DOM creation and removes the surface after exit motion. |
triggerValue / defaultTriggerValue | string | null | null | Tracks which value-bearing Trigger opened the drawer. |
id / ids | string / object | generated | Overrides machine and part identifiers. |
Drawer.Content
| Prop | Type | Default | Description |
|---|---|---|---|
draggable | boolean | true | Allows dragging from the complete surface. The Grabber still works when false. |
showGrabber | boolean | true | Renders the directional grabber and indicator. |
showBackdrop | boolean | true | Renders the dimmed modal backdrop. |
showCloseButton | boolean | false | Renders a built-in corner close control. |
closeLabel | string | "Close drawer" | Localizes the built-in close control's accessible name. |
teleport | boolean | true | Moves layers out of clipping and stacking contexts. |
teleportTo | Teleport target | "body" | Sets the Vue Teleport destination. |
Parts
| Part | Element | Description |
|---|---|---|
Drawer.Root | renderless | Owns open state, gestures, snap points, focus, modality, and dismissal. |
Drawer.RootProvider | renderless | Connects parts to an external useDrawer machine. |
Drawer.Stack | renderless | Coordinates visual state for nested drawer roots. |
Drawer.Trigger | button | Opens the drawer and can identify itself with a value. |
Drawer.SwipeArea | div | Optional viewport-edge gesture area that can open a closed drawer. |
Drawer.Content | div | Composes Teleport, Backdrop, Positioner, surface, Grabber, and optional close control. |
Drawer.Backdrop / Positioner | div | Public viewport layers used by composed Content. |
Drawer.Grabber / GrabberIndicator | div | Directional drag target and its visible indicator. |
Drawer.Header / Footer | div | Kappa layout parts for stable copy and action regions. |
Drawer.Title | h2 | Visible accessible name connected to Content. |
Drawer.Description | p | Visible accessible description connected to Content. |
Drawer.Close / CloseTrigger | button | Dismisses the drawer and restores focus. |
Drawer.Context | renderless | Exposes the unwrapped drawer API to its slot. |
Drawer.Indent / IndentBackground | div | Optional page layers driven by Drawer.Stack state. |
Events
| Event | Payload | Description |
|---|---|---|
update:open | boolean | Drives v-model:open. |
openChange | { open: boolean } | Reports every visibility change. |
update:snapPoint | number | string | null | Drives v-model:snapPoint. |
snapPointChange | { snapPoint: number | string | null } | Reports the active resting position. |
update:triggerValue | string | null | Drives v-model:triggerValue. |
triggerValueChange | { value: string | null; triggerElement: HTMLElement | null } | Reports the active value-bearing trigger. |
exitComplete | void | Fires after the closed-state motion completes. |
Exports
| Export | Description |
|---|---|
Drawer | Compound root API exposing every named part. |
DrawerRoot / DrawerContent / DrawerClose … | Named unaugmented component exports. |
DrawerProps / DrawerContentProps | Public root and composed-content prop contracts. |
DrawerSwipeDirection / DrawerSnapPoint / DrawerRole | Supported behavior value types. |
DrawerOpenChangeDetails and change-detail types | Typed Ark UI event payloads. |
DRAWER_SWIPE_DIRECTIONS / DRAWER_ROLES | Readonly supported-value lists. |
useDrawer / useDrawerContext / useDrawerStackContext | Ark UI hooks for external and descendant access. |
drawerAnatomy | Ark UI part anatomy metadata. |