Overview
Svelte Drawer provides comprehensive keyboard accessibility features out of the box, including keyboard shortcuts, focus management, and proper ARIA attributes.
Keyboard Shortcuts
Escape Key
By default, pressing Escape closes the drawer:
< script >
import { Drawer , DrawerOverlay , DrawerContent } from '@abhivarde/svelte-drawer' ;
let open = $ state ( false );
</ script >
< Drawer bind : open >
< DrawerOverlay class = "fixed inset-0 bg-black/40" />
< DrawerContent class = "fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4" >
< h2 > Press Escape to close </ h2 >
</ DrawerContent >
</ Drawer >
The implementation listens for the Escape key:
/home/daytona/workspace/source/src/lib/components/Drawer.svelte:145-150
function handleKeydown ( e : KeyboardEvent ) {
if ( open && closeOnEscape && e . key === "Escape" ) {
e . preventDefault ();
closeDrawer ();
}
}
Disabling Escape Key
You can disable the Escape key behavior:
< script >
import { Drawer , DrawerOverlay , DrawerContent } from '@abhivarde/svelte-drawer' ;
let open = $ state ( false );
</ script >
< Drawer bind : open closeOnEscape = { false } >
< DrawerOverlay class = "fixed inset-0 bg-black/40" />
< DrawerContent class = "fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4" >
< h2 > Cannot close with Escape </ h2 >
< p > User must click close button or overlay. </ p >
</ DrawerContent >
</ Drawer >
Only disable closeOnEscape if you provide an alternative way to close the drawer
Overlay Shortcuts
The overlay responds to Enter and Space keys when focused:
/home/daytona/workspace/source/src/lib/components/DrawerOverlay.svelte:29-34
function handleKeydown ( e : KeyboardEvent ) {
if ( e . key === "Enter" || e . key === " " ) {
e . preventDefault ();
drawer . closeDrawer ();
}
}
Focus Management
Auto-Focus
When the drawer opens, focus is automatically moved to the first focusable element:
/home/daytona/workspace/source/src/lib/components/DrawerContent.svelte:191-202
$effect (() => {
if ( drawer . open && trapFocus && contentElement ) {
tick (). then (() => {
const focusable = getFocusableElements ();
if ( focusable [ 0 ]) {
focusable [ 0 ]. focus ({ preventScroll: true });
} else {
contentElement ?. focus ({ preventScroll: true });
}
});
}
});
Focusable elements include:
Links with href attribute
Buttons (not disabled)
Form inputs, textareas, selects (not disabled)
Elements with tabindex (except -1)
/home/daytona/workspace/source/src/lib/components/DrawerContent.svelte:164-171
function getFocusableElements () : HTMLElement [] {
if ( ! contentElement ) return [];
return Array . from (
contentElement . querySelectorAll (
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
) as HTMLElement [];
}
Focus Trap
By default, focus is trapped inside the drawer - pressing Tab cycles through focusable elements:
< script >
import { Drawer , DrawerOverlay , DrawerContent } from '@abhivarde/svelte-drawer' ;
let open = $ state ( false );
</ script >
< Drawer bind : open >
< DrawerOverlay class = "fixed inset-0 bg-black/40" />
< DrawerContent class = "fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4" >
< h2 > Focus Trapped </ h2 >
< button > First Button </ button >
< button > Second Button </ button >
< button > Last Button </ button >
<!-- Tab wraps from last to first -->
</ DrawerContent >
</ Drawer >
The focus trap implementation:
/home/daytona/workspace/source/src/lib/components/DrawerContent.svelte:173-189
function handleFocusTrap ( e : KeyboardEvent ) {
if ( ! trapFocus || ! drawer . open || e . key !== "Tab" ) return ;
const focusable = getFocusableElements ();
if ( ! focusable . length ) return ;
const first = focusable [ 0 ];
const last = focusable [ focusable . length - 1 ];
if ( e . shiftKey && document . activeElement === first ) {
e . preventDefault ();
last . focus ();
} else if ( ! e . shiftKey && document . activeElement === last ) {
e . preventDefault ();
first . focus ();
}
}
Disabling Focus Trap
You can disable focus trapping if you need keyboard navigation outside the drawer:
< script >
import { Drawer , DrawerOverlay , DrawerContent } from '@abhivarde/svelte-drawer' ;
let open = $ state ( false );
</ script >
< Drawer bind : open >
< DrawerOverlay class = "fixed inset-0 bg-black/40" />
< DrawerContent trapFocus = { false } class = "fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4" >
< h2 > Tab navigation not restricted </ h2 >
< p > You can tab to elements outside the drawer. </ p >
</ DrawerContent >
</ Drawer >
The trapFocus prop is on DrawerContent, not on the root Drawer component
Focus Restoration
When the drawer closes, focus is automatically restored to the element that was focused before the drawer opened:
/home/daytona/workspace/source/src/lib/components/Drawer.svelte:94-96
if ( open ) {
visible = true ;
previouslyFocusedElement = document . activeElement as HTMLElement ;
/home/daytona/workspace/source/src/lib/components/Drawer.svelte:125-128
if ( previouslyFocusedElement ) {
previouslyFocusedElement . focus ();
previouslyFocusedElement = null ;
}
ARIA Attributes
The drawer includes proper ARIA attributes for screen readers:
/home/daytona/workspace/source/src/lib/components/DrawerContent.svelte:214-222
< div
bind : this = { contentElement }
class = { className }
style = "transform: { getTransform () } ; z-index: 50; touch-action: none;"
tabindex = "-1"
role = "dialog"
aria-modal = "true"
onpointerdown = { onPointerDown }
ontouchstart = { onPointerDown }
role="dialog" - Identifies the drawer as a dialog
aria-modal="true" - Indicates the drawer is modal
tabindex="-1" - Allows programmatic focus without tab navigation
The overlay also includes ARIA attributes:
/home/daytona/workspace/source/src/lib/components/DrawerOverlay.svelte:37-47
< div
class = "fixed inset-0 bg-black/40 cursor-pointer { blurClass () } { className } "
style = "opacity: { drawer . overlayOpacity . current } ; z-index: 40;"
onclick = { drawer . closeDrawer }
onkeydown = { handleKeydown }
role = "button"
tabindex = " 0 "
aria-label = "Close drawer"
{ ... restProps }
></ div >
Complete Example
Here’s a fully accessible drawer with all features:
< script >
import { Drawer , DrawerOverlay , DrawerContent , DrawerHandle , DrawerHeader } from '@abhivarde/svelte-drawer' ;
let open = $ state ( false );
</ script >
< button onclick = { () => open = true } >
Open Accessible Drawer
</ button >
< Drawer bind : open closeOnEscape = { true } >
< DrawerOverlay class = "fixed inset-0 bg-black/40" />
< DrawerContent trapFocus = { true } class = "fixed bottom-0 left-0 right-0 bg-white rounded-t-lg" >
< DrawerHeader
title = "Accessible Drawer"
description = "Fully keyboard accessible"
showCloseButton = { true }
/>
< div class = "p-4" >
< button > First Button </ button >
< button > Second Button </ button >
< button onclick = { () => open = false } > Close </ button >
</ div >
</ DrawerContent >
</ Drawer >
Keyboard Navigation Summary
Key Action Can Disable EscapeClose drawer closeOnEscape={false}TabNext focusable element trapFocus={false}Shift+TabPrevious focusable element trapFocus={false}Enter (on overlay)Close drawer No Space (on overlay)Close drawer No
Best Practices
Always provide a visible close button in addition to keyboard shortcuts
Keep focus trap enabled (default) for better accessibility
Test your drawer with keyboard-only navigation (no mouse)
If you disable closeOnEscape, ensure there’s an obvious way to close the drawer
Focus management works automatically - you don’t need to write any additional code
API Reference Complete prop documentation
DrawerContent API Focus trap and accessibility props