use-swipe-to-close
v1.0.2
Published
React hook for native-like swipe-to-close bottom sheets and modals
Maintainers
Readme
use-swipe-to-close
A React hook for building native-like, swipe-to-close bottom sheets and modals with smooth gestures and smart scroll handling.
✨ Features
- 📱 Native Feel: Spring animations and physics that match iOS and Android behavior.
- 🔗 Smart Scroll Chaining: If the content is scrolled, swiping down scrolls it to the top first, then pulls the sheet. No accidental closes!
- ⚡ High Performance: Direct
transformmanipulation using the Web Animations API. No heavy third-party animation libraries. - 🛡️ Robust Touch Handling: Prevents text selection, ignores multi-touch conflicts, and allows seamlessly interrupting animations mid-flight.
- 🪶 Zero Dependencies: Pure React and browser APIs.
📦 Installation
npm install use-swipe-to-close
# or
yarn add use-swipe-to-close
# or
pnpm add use-swipe-to-close🚀 Usage
Here is a complete, minimal example of how to use the hook.
import React, { useState, useEffect } from 'react';
import { useSwipeToClose } from 'use-swipe-to-close';
export default function BottomSheetDemo() {
const [isOpen, setIsOpen] = useState(false);
const { sheetRef, contentRef, animateOpen, animateClose } = useSwipeToClose({
isOpen,
onClose: () => setIsOpen(false),
threshold: 0.25, // Closes if dragged down more than 25% of the sheet's height
isEnabled: true,
});
// Trigger open animation when state changes
useEffect(() => {
if (isOpen) {
// Double RAF ensures the DOM is fully painted before animating
requestAnimationFrame(() => {
requestAnimationFrame(() => animateOpen());
});
}
}, [isOpen, animateOpen]);
if (!isOpen) {
return <button onClick={() => setIsOpen(true)}>Open Bottom Sheet</button>;
}
return (
<div className="overlay" onClick={() => animateClose()}>
<div
ref={sheetRef}
className="sheet"
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside the sheet
>
<div className="handle" />
{/* Attach contentRef to the scrollable container! */}
<div ref={contentRef} className="content">
<h2>Sheet Title</h2>
<p>Swipe down to close. If you scroll down first, swiping down will scroll the content back to the top before closing the sheet.</p>
{Array.from({ length: 20 }).map((_, i) => (
<div key={i} className="card">
<h3>Item {i + 1}</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
</div>
))}
</div>
</div>
</div>
);
}🎨 Required CSS
For the hook to work perfectly and prevent page bounce/scrolling issues, apply these essential styles to your components:
/* The main sheet container */
.sheet {
touch-action: none; /* We control gestures via JS */
user-select: none; /* Prevent text selection on rapid taps */
-webkit-user-select: none;
-webkit-touch-callout: none;
will-change: transform;
}
/* The scrollable content inside the sheet */
.content {
overflow-y: auto;
-webkit-overflow-scrolling: touch; /* Smooth inertial scrolling on iOS */
overscroll-behavior: contain; /* Prevents the body behind the sheet from scrolling */
}
/* Basic overlay styling (optional) */
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: flex-end; /* Bottom sheet alignment */
}