small, unstyled primitives for floating UI. No providers. No design system. Tree-shakable.
You know the drill. You need a floating chat button, a side dock, a pull-up sheet, or a resizable split layout. You search npm. You find one of two things:
-
A 40KB monolith that ships with its own design system, demands a
<Provider>at the root, and renders animated 3D bevels on a button you wanted to style yourself. -
A drag library so low-level you spend the next four hours wiring up viewport clamping, touch events, snap-on-release, and a
ResizeObserverso the thing doesn't fly off-screen when the user rotates their phone.
Neither is what I want. I want a thing that handles the positioning and interaction, hands me a <div>, and gets out of the way.
That's react-driftkit.
npm install react-driftkit
The design rule: one component, one job
react-driftkit ships four primitives. Each one is independent, tree-shakable, unstyled, and a few KB gzipped. You import only what you use.
| Component | What it does |
|---|---|
<MovableLauncher> |
Drag-anywhere floating wrapper. Optional snap-to-corner. |
<SnapDock> |
Edge-pinned dock that flips between horizontal and vertical when you drag it across edges. |
<DraggableSheet> |
Pull-up/down sheet with peek / half / full snap points. |
<ResizableSplitPane> |
N-pane resizable layout with localStorage-persisted ratios. |
No <DriftProvider>. No theme tokens. No CSS file you have to import. Your styles, your structure — the kit owns positioning and pointer math.
The four primitives, with code
1. <MovableLauncher> — the floating chat button, done right
import { MovableLauncher } from 'react-driftkit';
<MovableLauncher defaultPosition="bottom-right" snapToCorners>
<button className="my-chat-btn">💬</button>
</MovableLauncher>
That's the whole API for the common case. It handles:
- Mouse, touch, and pen via the Pointer Events API (one code path, not three)
- A 5px drag threshold so nested
<button>s still receive clicks - Auto-reposition when the viewport resizes or the child's size changes
- A bouncy snap-to-nearest-corner on release (when you opt in)
If you want pixel-precise control instead of corners:
<MovableLauncher defaultPosition={{ x: 100, y: 200 }}>
<Toolbar />
</MovableLauncher>
That's it. The wrapper is a position: fixed <div> you can style however you want.
2. <SnapDock> — VS Code / Figma-style edge dock
This is the one I'm most proud of. Drag it to any edge of the viewport and it pins there. Drag it from the bottom edge to the left edge, and the layout flips from horizontal to vertical with a FLIP-style animation anchored to the active edge.
<SnapDock defaultEdge="left" shadow>
<button>Home</button>
<button>Search</button>
<button>Settings</button>
</SnapDock>
The wrapper exposes data-edge and data-orientation attributes, so you can drive any CSS you want without re-rendering:
.my-dock[data-orientation="vertical"] {
flex-direction: column;
}
Want to react in JS too?
<SnapDock
defaultEdge="left"
onEdgeChange={(edge) => console.log('moved to', edge)}
onOffsetChange={(offset) => console.log('offset', offset)}
>
<Toolbar />
</SnapDock>
3. <DraggableSheet> — bottom sheets that don't suck on mobile
If you've ever tried to build a Maps-style detail sheet from scratch, you know the pain: drag tracking, velocity-based snap, scroll containment, the works.
<DraggableSheet snapPoints={['peek', 'half', 'full']} defaultSnap="half">
<div data-handle className="sheet-handle" />
<div className="sheet-body">Filters, cart, details…</div>
</DraggableSheet>
Mix presets, raw pixels, and percentages in the same array. They get resolved against the drag axis at gesture time:
<DraggableSheet snapPoints={['peek', 200, '40%', 'full']} />
Velocity matters. A fast flick advances one stop in the flick direction; a slow drag snaps to the nearest one. The dragHandleSelector prop lets you confine drag to a handle strip so the sheet body keeps scrolling normally:
<DraggableSheet
snapPoints={['peek', 'half', 'full']}
dragHandleSelector="[data-handle]"
>
<div data-handle className="handle-strip" />
<div className="scroll-area">{/* scrolls normally */}</div>
</DraggableSheet>
4. <ResizableSplitPane> — for editor and dashboard layouts
Two or more children, one drag handle between each adjacent pair. Persistence built in.
<ResizableSplitPane
defaultSizes={[0.25, 0.5, 0.25]}
persistKey="editor-layout"
>
<FileTree />
<Editor />
<Preview />
</ResizableSplitPane>
persistKey writes the ratios to localStorage. Refresh the page, layout sticks. Double-click any handle to reset to defaults. Drag handles use a 3px threshold and pointer events, same as the rest.
Custom handles? One render prop, called per boundary:
<ResizableSplitPane
handle={({ index, isDragging, orientation }) => (
<div style={{ background: isDragging ? '#6366f1' : '#e5e7eb' }} />
)}
>
<Sidebar />
<Main />
</ResizableSplitPane>
The boring stuff that actually matters
This is the part I rarely see in floating-UI libraries:
-
Pointer Events, not three event systems. Mouse, touch, and pen go through one code path. No
onTouchStart+onMouseDownduplication. -
ResizeObservereverywhere. When your child's size changes — say, a chat widget expands when opened — the wrapper re-clamps to stay on-screen. No more launchers stuck half off the right edge. -
z-index: 2147483647. Yes, the max. Because if I'm a floating UI library, I'm above your modal. Stop fighting me. -
Tree-shaking actually works. Import
MovableLauncherand you getMovableLauncher. You do not get the sheet, the dock, or the splitter pulled in by accident. -
TypeScript-first. All props, all types, all generics exported.
Edge,SnapPoint,HandleInfo— pull them in by name.
What it doesn't do (on purpose)
-
No styling. Zero. You bring your background, padding, gap, shadows. The kit gives you data attributes (
data-edge,data-orientation,data-snap,data-dragging) so you can style state without re-rendering. - No context providers. Drop a component anywhere, it works.
-
No animation library. Transforms and CSS transitions only. The FLIP animation in
SnapDockis hand-rolled in ~30 lines because pulling in Framer Motion to flip a dock would be insane. - No "kitchen sink" component. If you want something that's a floating launcher and a dock and a sheet, you compose them. They don't know about each other and they don't need to.
When you'd actually reach for this
- Building a chat widget or feedback button that users can move out of their way
- Shipping a floating toolbar in a doc/canvas editor
- Building a mobile detail sheet — filters, cart, product details
- Adding an inspector or debug panel to your app
- Replacing your hand-rolled, flexbox-and-
mousemoveresizable sidebar with something that handles touch and persists across sessions - Anything VS Code / Figma / Linear-shaped
If you need a full design system, this isn't it. If you need a single, well-scoped primitive, this is exactly it.
Try it
The fastest way to see what it feels like is the live demo — drag every component around, snap them between edges, resize the panes.
Then:
npm install react-driftkit
- npm: react-driftkit
- GitHub: shakcho/react-drift
- Demo: react-driftkit.saktichourasia.dev
What's next
This is v0.4 and the kit is actively growing. More small, single-purpose floating-UI primitives are on the way — same design rules: tree-shakable, unstyled, one component per job. No bloat, no providers, no design-system lock-in.
Two things would help a lot:
- ⭐ Star the repo on GitHub. It's the clearest signal for what to prioritize next, and it helps other devs find the library.
- Open a GitHub issue with feature requests. If there's a floating-UI primitive you keep rebuilding by hand — a draggable modal, a magnetic snap-grid, anything — file an issue. Real use cases drive the roadmap. The next component is whatever the most people are asking for.
Bug reports and PRs equally welcome.
If this post helped, a star on GitHub, and an issue with what you'd build next all go a long way.
This article was originally published by DEV Community and written by Sakti Chourasia.
Read original article on DEV Community