A performant React bottom sheet with reliable scroll-vs-drag detection. Zero dependencies. Compound components. TypeScript first.
The core challenge is the scroll-to-drag transition. When scrollable content reaches the top and you keep pulling down, the sheet should follow your finger. Most libraries fail at this, resulting in rubber-band bouncing, jerky transitions, or the sheet ignoring the gesture entirely.
Try them right here. Drag, scroll, snap.
Open, drag, close
Pixel and fraction values
Seamless scroll-to-drag
Everything you need, nothing you don't.
When scrollable content reaches the top, the sheet seamlessly follows your finger down. No rubber-band bounce.
Only React as peer dependency. No motion library, no gesture library, no UI framework required.
Fractions (0.5 = 50vh) and pixel values ('200px'). Sequential snapping for step-by-step navigation.
Stack multiple sheets with automatic scale effect, just like native iOS sheet stacking.
Detached from screen edges with rounded corners. Customizable via CSS custom properties.
A companion bar that floats above the sheet and follows it during drag. Auto fades and slides away.
Overlay opacity follows the drag position in real time instead of a binary on/off transition.
Focus trap in modal mode, ESC to close, aria-labelledby, aria-describedby. Screen reader ready.
Side by side with other React bottom sheet libraries.
| GlideSheet | Vaul | react-modal-sheet | react-spring-bs | |
|---|---|---|---|---|
| Bundle | ~45KB | ~77KB | ~30KB + Motion | ~25KB + react-spring |
| Dependencies | 0 | Radix Dialog | Motion | react-spring + gesture |
| Scroll-to-drag | Native feel | Inconsistent | Manual config | Manual config |
| Non-modal | Clean | pointer-events leak | Not built-in | blocking={false} |
| iOS keyboard | visualViewport | position: fixed | avoidKeyboard | --- |
| Nested sheets | Built-in | Built-in | --- | --- |
| Floating mode | Built-in | --- | --- | --- |
| FloatingBar | Built-in | --- | --- | --- |
| Progressive overlay | Built-in | --- | --- | --- |
| React 19 | Yes | Yes | Yes | No |
Up and running in under a minute.
import { BottomSheet } from 'glidesheet';
import 'glidesheet/style.css';
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
<BottomSheet.Root open={open} onOpenChange={setOpen}>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content>
<BottomSheet.Handle />
<h2>Hello from GlideSheet</h2>
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet.Root>
</>
);
}<BottomSheet.Root
open={open}
onOpenChange={setOpen}
snapPoints={[0, '200px', 0.5, 1]}
activeSnapPoint={snap}
onActiveSnapPointChange={setSnap}
>
<BottomSheet.Portal>
<BottomSheet.Content>
<BottomSheet.Handle />
{/* Your content */}
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet.Root>