Coming from Vaul, shadcn Sheet, react-sliding-pane, or hand-rolled Framer Motion panels, these are the behaviors you get without wiring:
- Edge swipe to open. Drag from the edge on touch. No bolting gesture libraries on top of a dialog.
- Drag to close with velocity commit. Flick to dismiss, soft release snaps back. Native iOS and Android drawer feel.
- Spring physics, no motion library. The animation engine is built in. Zero runtime dependencies.
- Mouse and touch parity. The same gesture works on desktop pointers, not only mobile.
- Multi instance per side. Two or more sidebars on the same edge with independent state.
- Bottom sheet with mid anchor. Half-open stop and full-open stop, like native sheets.
- Accessible. Focus trap, Escape to close, aria attributes, keyboard nav.
Full comparisons: vs Vaul (same category gesture drawer) · vs shadcn sheet (modal dialog vs drawer)
npm install @luciodale/swipe-barimport { SwipeBarProvider, SwipeBarLeft } from "@luciodale/swipe-bar";
function App() {
return (
<SwipeBarProvider>
<SwipeBarLeft className="bg-gray-900 text-white">
<nav>
<a href="/dashboard">Dashboard</a>
<a href="/settings">Settings</a>
</nav>
</SwipeBarLeft>
<main>Your app content</main>
</SwipeBarProvider>
);
}Swipe from the left edge on mobile or click the toggle on desktop. That's it.
- Zero dependencies — just React
- Left, right, and bottom — all three directions with the same API
- Native touch gestures — edge swipe detection, drag tracking, velocity commit/cancel
- Multi-instance — multiple sidebars per direction with independent state via
idprop - Bottom sheets with mid-anchor — swipe to a halfway stop, then again to fully open
- Typed sidebar metadata — attach a generic type map and get compile-time safety
- Programmatic control — open, close, and read state from anywhere via context hook
- Cross-direction locking — one direction at a time, no gesture conflicts
- Accessibility — focus trap, Escape to close, aria attributes, keyboard navigation
- Runtime configuration — change any prop at runtime via
setGlobalOptions
import { useSwipeBarContext } from "@luciodale/swipe-bar";
function Header() {
const { openSidebar, closeSidebar, isLeftOpen } = useSwipeBarContext();
return (
<header>
<button onClick={() => openSidebar("left")}>Menu</button>
</header>
);
}import { SwipeBarBottom } from "@luciodale/swipe-bar";
<SwipeBarBottom sidebarHeightPx={400} isAbsolute midAnchorPoint>
<div>Sheet content</div>
</SwipeBarBottom>Give each sidebar a unique id. Each instance operates independently.
<SwipeBarLeft id="nav" isAbsolute>
<nav>Navigation</nav>
</SwipeBarLeft>
<SwipeBarLeft
id="settings"
isAbsolute
swipeToOpen={false}
showToggle={false}
swipeBarZIndex={70}
overlayZIndex={65}
>
<div>Settings panel</div>
</SwipeBarLeft>const { openSidebar, leftSidebars } = useSwipeBarContext();
openSidebar("left", { id: "settings" });
const isSettingsOpen = leftSidebars.settings?.isOpen ?? false;Full documentation, configuration reference, and live examples at koolcodez.com/projects/swipe-bar.
MIT