@get-set/gs-drawer
v1.0.2
Published
Get-Set Drawer
Downloads
170
Maintainers
Readme
@get-set/gs-drawer
A beautiful, fully-featured off-canvas drawer / sidebar with four usage modes from one codebase:
window.GSDrawer(selectorOrElement, params)— native factory$(selector).GSDrawer(params)— jQuery pluginelement.GSDrawer(params)—HTMLElement.prototypemethod<GSDrawer>— React component with a typed imperativeref
All four share the same actions/, constants/, helpers/ and types/, so
behaviour is identical everywhere. It slides in from any edge with polished,
side-aware animations, a rich backdrop layer catalog (shared with GSModal),
push-content mode, swipe-to-close, full keyboard + ARIA accessibility,
light / dark / auto theming, RTL, and sensible defaults.
Features
- Four anchor sides —
left·right·top·bottom. - Sizes —
sm·md·lg·xl·full, or an explicitwidth/height. - Backdrop layer catalog —
dim·blur·glass·gradient·vignette·tint·none, on top of thebackdrop/backdropColor/backdropOpacity/backdropBlurprimitives. - Beautiful animations — side-aware
slide/slide-over/reveal/flip/swing, plusfade/scale; independent enter/exit, per-instance duration & easing,prefers-reduced-motionaware. - Push content — translate the page aside instead of overlaying it.
- Swipe to close — drag the panel toward its edge; release past a threshold to dismiss.
- Header / footer — optional
title,subtitle, footer HTML; sticky while the body scrolls. - Dismissal control —
closeOnBackdrop,closeOnEsc,persistent,'static'backdrop (shakes). - Focus management — focus trap, auto-focus,
initialFocus, return-focus on close. - Scroll lock — body scroll lock with scrollbar-gap compensation.
- Theming —
light/dark/auto(+ accent color), RTL. - Stacking — nested drawers layer with incrementing z-index; Esc closes the topmost.
- React-only
gsx— scoped per-instance styles injected/removed automatically. - Instance registry —
window.GSDrawerConfigue.instance(key).
Compatibility
- Modern browsers (ES2020).
- React 16.8+ / 17 / 18 / 19 (peer dependency, optional — the native/jQuery/prototype modes need no React).
- Ships a bundled
dist-js/bundle.jsfor<script>/ jQuery / prototype use, and an ESMdist/build for React/bundlers.
Project layout
GSDrawer.ts window.GSDrawer factory (native entry)
actions/ init · open · close · destroy · refresh · animate · focusTrap · getCurrentParams
constants/ animations · sizes · defaultParams
helpers/ layout · swipe · uihelpers
types/ params · ref · global.d.ts
components/GSDrawer.tsx React component (forwardRef + GSDrawerHandle)
components/styles/ GSDrawer.css · .scss · GSDrawerCSS.ts (injected by React)
styles/ GSDrawer.css · .scss (for native/<link>)
example.html native showcase
tests/ vitest suites (logic · native · react)Install
npm i @get-set/gs-drawerBuild & test
npm run build # webpack bundle (dist-js) + tsc React build (dist)
npm test # vitestThe build emits no .map files and contains no sourceMappingURL.
Usage — Native JS
The factory accepts a CSS selector string OR a DOM element as its first
argument, then a params object. It wraps the element in an off-canvas panel and
returns a Ref with imperative methods.
<link rel="stylesheet" href="node_modules/@get-set/gs-drawer/styles/GSDrawer.css" />
<script src="node_modules/@get-set/gs-drawer/dist-js/bundle.js"></script>
<button id="open">Menu</button>
<div id="menu" hidden>
<nav>… your drawer content …</nav>
</div>
<script>
// By selector string:
const drawer = window.GSDrawer('#menu', {
reference: 'main-nav',
side: 'left',
size: 'md',
title: 'Navigation',
trigger: '#open',
backdropType: 'blur',
animation: 'slide'
});
// …or by element (identical):
// const drawer = window.GSDrawer(document.getElementById('menu'), { ... });
drawer.openDrawer();
drawer.toggle();
drawer.isOpen(); // -> boolean
drawer.closeDrawer();
</script>jQuery
$('#menu').GSDrawer({ side: 'right', title: 'Cart', trigger: '#open-cart' });HTMLElement prototype
document.getElementById('menu').GSDrawer({ side: 'left', swipeToClose: true });Both adapters pass the element (not a selector) into the factory — which is why the factory must accept an element.
Instance methods (registry)
Every instance is registered in window.GSDrawerConfigue. Retrieve one by its
reference and drive it imperatively:
const ref = window.GSDrawerConfigue.instance('main-nav');
ref.openDrawer();
ref.closeDrawer();
ref.toggle();
ref.isOpen();
ref.setContent('<p>Replaced body</p>');
ref.refresh(); // rebuild from (possibly mutated) params
ref.destroy(); // teardown + restore content + unregisterUsage — React
import { useRef } from 'react';
import GSDrawer, { GSDrawerHandle } from '@get-set/gs-drawer';
function App() {
const drawer = useRef<GSDrawerHandle>(null);
return (
<>
<button onClick={() => drawer.current?.open()}>Open menu</button>
<GSDrawer
ref={drawer}
reference="main-nav"
side="left"
size="md"
title="Navigation"
subtitle="Where to?"
backdropType="blur"
animation="slide"
swipeToClose
onOpen={() => console.log('opened')}
onClose={() => console.log('closed')}>
<nav>
<a href="#">Dashboard</a>
<a href="#">Projects</a>
<a href="#">Settings</a>
</nav>
</GSDrawer>
</>
);
}Controlled mode + external trigger:
const [open, setOpen] = useState(false);
<button ref={btnRef}>Open</button>
<GSDrawer
open={open}
onClose={() => setOpen(false)}
trigger={btnRef}
side="right">
…
</GSDrawer>Next.js (App Router)
The component renders through a portal and guards SSR (canPortal), so it works
in the App Router. Mark the file that renders it as a client component:
'use client';
import GSDrawer from '@get-set/gs-drawer';gsx — scoped styles (React only)
gsx injects a per-instance <style data-key="…"> scoped to this drawer's
overlay via [data-key], removed automatically on unmount.
<GSDrawer
reference="fancy"
gsx={{
'.gs-drawer-panel': { borderRadius: '0 20px 20px 0' },
'.gs-drawer-title': { letterSpacing: '0.02em' }
}}>
…
</GSDrawer>Options / props
Native params and React props share the same names (React adds open,
defaultOpen, onOpen, onClose, gsx, and children).
Identity
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| reference | string | auto GUID | Registry key. Reused as the data-key on the overlay. |
| gsx | NestedCSS | — | React only. Scoped inline styles injected for this instance. |
Placement & sizing
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| side | 'left' \| 'right' \| 'top' \| 'bottom' | 'left' | Edge the drawer is anchored to and slides from. |
| size | 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full' | 'md' | Width for left/right; height for top/bottom. |
| width | number \| string | — | Explicit width (overrides size for left/right). Number = px. |
| height | number \| string | — | Explicit height (overrides size for top/bottom). Number = px. |
Behaviour
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| closeOnBackdrop | boolean | true | Close when the backdrop is clicked. |
| closeOnEsc | boolean | true | Close on Escape (topmost drawer only). |
| persistent | boolean | false | Non-dismissable: backdrop/Esc/× no longer close (only programmatic close). |
| pushContent | boolean | false | Translate the page aside instead of overlaying it. |
| pushSelector | selector \| HTMLElement \| ref | body children | Element(s) to translate when pushContent is on. |
| swipeToClose | boolean | false | Enable edge-drag / swipe-to-close on the panel. |
| swipeThreshold | number | 0.4 | Fraction (0–1) of the panel size past which a release closes. |
Backdrop
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| backdrop | boolean \| 'static' | true | true = dim + click-to-close; false = no overlay; 'static' = present but shakes instead of closing. |
| backdropType | catalog (below) | 'dim' | Rich backdrop layer preset. |
| backdropColor | string | rgba(17,24,39,.5) | Overlay scrim color. |
| backdropOpacity | number | 1 | Overlay alpha (0–1). |
| backdropBlur | boolean \| number \| string | false | true=8px; number=px; string=raw CSS length. |
Animation
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| animation | catalog (below) \| false | 'slide' | Entrance animation. false disables panel motion. |
| exitAnimation | catalog \| false | mirrors animation | Separate exit animation. |
| animationDuration | number \| { enter?, exit? } | 320 | Duration in ms; object sets each phase. |
| animationEasing | string | cubic-bezier(.32,.72,0,1) | CSS easing for the panel. |
| backdropAnimation | catalog \| false | 'fade' | Backdrop's own animation. |
| backdropAnimationDuration | number | 240 | Backdrop fade duration in ms. |
| reducedMotion | 'auto' \| boolean | 'auto' | Honor prefers-reduced-motion. 'auto' follows the OS. |
Presentation & close affordances
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| title | string | — | Header title (wires aria-labelledby). |
| subtitle | string | — | Secondary line under the title. |
| footer | string \| false | false | Footer HTML. |
| showClose | boolean | true | Show the built-in × close button. |
| closeButtonPosition | 'top-end' \| 'top-start' \| 'header' \| 'outside' | 'top-end' | × button placement (RTL-aware). |
Layout / scrolling
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| scrollable | boolean | true | Body scrolls independently; header/footer sticky. |
| stickyHeader | boolean | true | Pin the header while the body scrolls. |
| stickyFooter | boolean | true | Pin the footer while the body scrolls. |
| lockScroll | boolean | true | Lock <body> scroll while open. |
| scrollbarPadding | boolean | true | Compensate scrollbar width to avoid layout shift. |
Focus management
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| autofocus | boolean | true | Auto-focus the first focusable / [autofocus] on open. |
| initialFocus | selector \| HTMLElement \| ref \| false | — | Explicit element to focus. false focuses the panel. |
| disableFocus | boolean | false | Skip auto-focus entirely. |
| returnFocus | boolean | true | Restore focus to the previously focused element on close. |
Theming
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| theme | 'light' \| 'dark' \| 'auto' | 'auto' | Visual theme. auto follows prefers-color-scheme. |
| accentColor | string | #2563eb | Accent for focus ring / handle / close button (--gsd-accent). |
| rtl | boolean | false | Right-to-left layout. |
| customClass | GSDrawerCustomClass | — | Per-part class overrides (see below). |
| className | string | — | Extra class on the overlay root. |
Wiring
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| trigger | selector \| HTMLElement \| ref | — | Element(s) that open the drawer on click. |
| closeHandler | selector \| HTMLElement \| ref | — | Element(s) inside the content that close on click. |
Callbacks
| Option | Type | Description |
| --- | --- | --- |
| beforeOpen | () => void \| boolean | Return false to cancel opening. |
| afterOpen | () => void | Fired after the open transition. |
| beforeClose | () => void \| boolean | Return false to cancel closing. |
| afterClose | () => void | Fired after the drawer has fully closed. |
| onOpen / onClose | () => void | React only convenience callbacks. |
customClass parts
container · backdrop · panel · header · title · subtitle ·
closeButton · body · footer · handle.
Variant catalogs
Side catalog
| side | Anchors to | Slides from |
| --- | --- | --- |
| left | left edge, full height | the left |
| right | right edge, full height | the right |
| top | top edge, full width | the top |
| bottom | bottom edge, full width | the bottom |
Size catalog
| size | Slide-axis dimension |
| --- | --- |
| sm | 280px |
| md | 360px (default) |
| lg | 480px |
| xl | 640px |
| full | 100% |
width / height override the size on the relevant axis.
Backdrop type catalog
| backdropType | Look |
| --- | --- |
| dim | Plain dimmed scrim (honors backdropBlur). Default. |
| blur | Translucent scrim + 8px backdrop blur. |
| glass | Frosted light glass (blur + saturation). |
| gradient | Diagonal indigo→pink gradient scrim. |
| vignette | Darkened radial edges. |
| tint | Subtle accent-tinted scrim (uses accentColor). |
| none | No painted backdrop layer. |
Animation catalog
Side-aware animations bake the side into their keyframes automatically.
| animation | Side-aware | Description |
| --- | --- | --- |
| slide | ✓ | Clean edge slide. Default. |
| slide-over | ✓ | Slide with a slight fade. |
| reveal | ✓ | Slide + scale-from-edge with fade. |
| flip | ✓ | 3D hinge on the anchored edge. |
| swing | ✓ | Hinged swing with a little overshoot. |
| fade | — | Cross-fade in place. |
| scale | — | Scale + fade in place. |
| none | — | No motion (reduced-motion fallback). |
| false | — | Disable panel motion entirely. |
Theme catalog
| theme | Palette |
| --- | --- |
| light | Forced light. |
| dark | Forced dark. |
| auto | Follows prefers-color-scheme (default). |
Imperative API
React GSDrawerHandle (via ref)
| Method | Description |
| --- | --- |
| open() | Open the drawer. |
| close() | Close the drawer. |
| toggle() | Toggle open/closed. |
| isOpen() | boolean — current open state. |
| setContent(node) | Replace the body content with a React node. |
| destroy() | Close and unregister the instance. |
Native instance (Ref)
| Method | Description |
| --- | --- |
| openDrawer() | Open the drawer. |
| closeDrawer() | Close the drawer. |
| toggle() | Toggle open/closed. |
| isOpen() | boolean — current open state. |
| setContent(html \| element) | Replace the body content. |
| refresh() | Rebuild the overlay from current params (reopens if it was open). |
| destroy() | Teardown: close, remove listeners, restore content, unregister. |
Registry — window.GSDrawerConfigue
| Member | Description |
| --- | --- |
| references | Array<{ key, ref }> of every live instance. |
| instance(key) | Look up a Ref by its reference. |
Examples
Native — right-side cart, glass backdrop, swipe to close
const cart = window.GSDrawer('#cart', {
reference: 'cart',
side: 'right',
size: 'lg',
title: 'Your cart',
backdropType: 'glass',
swipeToClose: true,
trigger: '#open-cart',
footer: '<button id="checkout">Checkout</button>',
closeHandler: '#keep-shopping'
});Native — push-content sidebar
window.GSDrawer('#sidebar', {
side: 'left',
size: 'sm',
pushContent: true,
backdrop: false,
animation: 'slide',
trigger: '#hamburger'
});React — bottom sheet, dark, controlled
const [open, setOpen] = useState(false);
<GSDrawer
open={open}
onClose={() => setOpen(false)}
side="bottom"
size="sm"
theme="dark"
backdropType="vignette"
animation="slide"
title="Filters">
<FilterForm />
</GSDrawer>License
ISC
