Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,27 @@ jobs:
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

# Keep llms.txt in sync with the version that is about to be
# published, so the source file, the demo build, and npm are all
# aligned in the same release commit.
if [ -f llms.txt ]; then
NEW_VERSION="$VERSION" node -e '
const fs = require("fs");
const path = "llms.txt";
const v = process.env.NEW_VERSION;
const src = fs.readFileSync(path, "utf8");
const out = src.replace(
/(Current documented version:\s*`)[^`]+(`)/,
`$1${v}$2`,
);
if (out !== src) fs.writeFileSync(path, out);
'
fi

- name: Commit and tag
if: steps.bump.outputs.type != 'none'
run: |
git add package.json package-lock.json
git add package.json package-lock.json llms.txt
git commit -m "release: v${{ steps.version.outputs.version }}"
git tag "v${{ steps.version.outputs.version }}"
git push origin main --tags
Expand Down
223 changes: 179 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

# react-driftkit

**A lightweight, draggable floating widget wrapper for React.**
Snap to corners, drag anywhere, stay in bounds — all under 3KB.
**Small, focused building blocks for floating UI in React.**
Tree-shakable, unstyled, one component per job.

[![npm version](https://img.shields.io/npm/v/react-driftkit)](https://www.npmjs.com/package/react-driftkit)
[![npm downloads](https://img.shields.io/npm/dm/react-driftkit)](https://www.npmjs.com/package/react-driftkit)
Expand All @@ -18,21 +18,16 @@ Snap to corners, drag anywhere, stay in bounds — all under 3KB.

## Why react-driftkit?

Building a chat widget, floating toolbar, or debug panel? You need it to be draggable, stay on screen, and not fight your existing styles. Most draggable libraries are either too heavy, too opinionated, or don't handle edge cases like viewport resizing and touch input.
Building a chat widget, floating toolbar, debug panel, or side dock? You want these things to be draggable, stay on screen, and stay out of your way stylistically. Most draggable libraries are either too heavy, too opinionated, or don't handle edge cases like viewport resizing, touch input, and orientation changes.

**react-driftkit** solves exactly this — one component, zero config, works everywhere.
**react-driftkit** ships each pattern as its own tiny component. Import only what you use — every component is tree-shakable and under a few KB gzipped. All visuals are yours; the kit owns positioning and interaction.

## Features
## Components

- **Drag anywhere** — smooth pointer-based dragging with mouse, touch, and pen support
- **Snap to corners** — optional bounce-animated snapping to the nearest viewport corner
- **Smart positioning** — named corners (`top-left`, `bottom-right`, ...) or custom `{ x, y }` coordinates
- **Viewport-aware** — auto-repositions on window resize and content size changes
- **5px drag threshold** — distinguishes clicks from drags, so child buttons still work
- **Zero dependencies** — only React as a peer dependency
- **Tiny bundle** — under 3KB gzipped
- **TypeScript-first** — fully typed props and exports
- **Works with React 18 and 19**
| Component | What it does |
|-----------|--------------|
| [`<MovableLauncher>`](#movablelauncher) | A draggable floating wrapper that pins to any viewport corner or lives at custom `{x, y}` — drop-anywhere with optional snap-on-release. |
| [`<SnapDock>`](#snapdock) | An edge-pinned dock that slides along any side of the viewport and flips orientation automatically between horizontal and vertical. |

## Installation

Expand Down Expand Up @@ -60,44 +55,60 @@ bun add react-driftkit
## Quick Start

```tsx
import { MovableLauncher } from 'react-driftkit';
import { MovableLauncher, SnapDock } from 'react-driftkit';

function App() {
return (
<MovableLauncher defaultPosition="bottom-right">
<button>Chat with us</button>
</MovableLauncher>
<>
<MovableLauncher defaultPosition="bottom-right">
<button>Chat with us</button>
</MovableLauncher>

<SnapDock defaultEdge="bottom" shadow>
<button>Home</button>
<button>Search</button>
<button>Settings</button>
</SnapDock>
</>
);
}
```

That's it. Your button is now a draggable floating widget pinned to the bottom-right corner.
Both components are tree-shakable — import only what you use.

## Examples
---

## MovableLauncher

A draggable floating wrapper that lets users pick up any widget and drop it anywhere on the viewport — or snap it to the nearest corner on release.

### Features

### Snap to Corners
- **Drag anywhere** — pointer-based, works with mouse, touch, and pen
- **Snap to corners** — optional bounce-animated snap to the nearest viewport corner
- **Named or custom positioning** — `'top-left'`, `'bottom-right'`, or `{ x, y }`
- **Viewport-aware** — auto-repositions on window resize and child size changes
- **5 px drag threshold** — distinguishes clicks from drags so nested buttons still work

Release the widget and it bounces to the nearest corner:
### Examples

#### Snap to corners

```tsx
<MovableLauncher defaultPosition="bottom-right" snapToCorners>
<div className="my-widget">Drag me!</div>
</MovableLauncher>
```

### Free Positioning

Place the widget at exact coordinates:
#### Free positioning

```tsx
<MovableLauncher defaultPosition={{ x: 100, y: 200 }}>
<div className="toolbar">Toolbar</div>
</MovableLauncher>
```

### Styled Widget

Combine with your own styles and classes:
#### Styled widget

```tsx
<MovableLauncher
Expand All @@ -114,17 +125,15 @@ Combine with your own styles and classes:
</MovableLauncher>
```

## API Reference

### `<MovableLauncher>`
### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | *required* | Content to render inside the draggable wrapper |
| `defaultPosition` | `Corner \| { x, y }` | `'bottom-right'` | Initial position — a named corner or pixel coordinates |
| `snapToCorners` | `boolean` | `false` | Snap to the nearest viewport corner on release |
| `style` | `CSSProperties` | `{}` | Inline styles merged with the wrapper |
| `className` | `string` | `''` | CSS class added to the wrapper |
| `children` | `ReactNode` | *required* | Content rendered inside the draggable container. |
| `defaultPosition` | `Corner \| { x, y }` | `'bottom-right'` | Initial position — a named corner or pixel coordinates. |
| `snapToCorners` | `boolean` | `false` | Snap to the nearest viewport corner on release. |
| `style` | `CSSProperties` | `{}` | Inline styles merged with the wrapper. |
| `className` | `string` | `''` | CSS class added to the wrapper. |

### Types

Expand All @@ -137,31 +146,157 @@ interface Position {
}
```

### CSS Classes

The wrapper element exposes these classes for styling:
### CSS classes

| Class | When |
|-------|------|
| `movable-launcher` | Always present |
| `movable-launcher--dragging` | While the user is actively dragging |

---

## SnapDock

An edge-pinned dock that slides along any side of the viewport. Drag it anywhere — on release it snaps to the nearest edge and automatically flips between horizontal (top/bottom) and vertical (left/right) layouts. The layout change animates via a FLIP-style transition anchored to the active edge.

### Features

- **Edge pinning** — `left`, `right`, `top`, `bottom`, with a `0..1` offset along the edge
- **Automatic orientation** — children lay out in a row or column based on the current edge
- **Animated flip** — cross-edge drops animate smoothly from the old footprint to the new one
- **Drag anywhere** — same 5 px pointer threshold as MovableLauncher
- **`shadow` prop** — adds a sensible default drop shadow, overridable via `style.boxShadow`
- **Zero built-in visuals** — you supply the background, padding, gap, etc. via `style` or `className`
- **`data-edge` / `data-orientation` attributes** — flip your CSS layout without re-rendering

### Examples

#### Basic dock

```tsx
<SnapDock defaultEdge="left">
<MyToolbar />
</SnapDock>
```

#### Styled dock with shadow

```tsx
<SnapDock defaultEdge="bottom" shadow className="my-dock">
<button>Home</button>
<button>Search</button>
<button>Settings</button>
</SnapDock>
```

```css
.my-dock {
background: #111;
color: #fff;
padding: 8px;
border-radius: 12px;
gap: 6px;
}
```

`SnapDock` already sets `display: flex` and `flex-direction` based on the active edge, so you don't need to write orientation CSS yourself — but if you want to, the wrapper exposes `data-orientation="vertical" | "horizontal"`.

#### Tracking edge and offset changes

```tsx
import { useState } from 'react';
import { SnapDock, type Edge } from 'react-driftkit';

function App() {
const [edge, setEdge] = useState<Edge>('left');

return (
<SnapDock
defaultEdge={edge}
onEdgeChange={setEdge}
onOffsetChange={(offset) => console.log('offset', offset)}
>
<Toolbar />
</SnapDock>
);
}
```

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | *required* | Content rendered inside the dock. |
| `defaultEdge` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'left'` | Which edge the dock pins to initially. |
| `defaultOffset` | `number` | `0.5` | Position along the edge, from `0` (top/left) to `1` (bottom/right). |
| `snap` | `boolean` | `true` | Snap to the nearest edge on release. |
| `draggable` | `boolean` | `true` | Whether the user can drag the dock. |
| `edgePadding` | `number` | `16` | Distance in pixels from the viewport edge. |
| `shadow` | `boolean` | `false` | Adds a default drop shadow. Override via `style.boxShadow`. |
| `onEdgeChange` | `(edge: Edge) => void` | — | Fires when the dock moves to a new edge. |
| `onOffsetChange` | `(offset: number) => void` | — | Fires when the dock's offset along the edge changes. |
| `style` | `CSSProperties` | `{}` | Inline styles merged with the wrapper. |
| `className` | `string` | `''` | CSS class added to the wrapper. |

### Types

```typescript
type Edge = 'left' | 'right' | 'top' | 'bottom';
type Orientation = 'vertical' | 'horizontal';

interface SnapDockProps {
children: ReactNode;
defaultEdge?: Edge;
defaultOffset?: number;
draggable?: boolean;
snap?: boolean;
edgePadding?: number;
shadow?: boolean;
onEdgeChange?: (edge: Edge) => void;
onOffsetChange?: (offset: number) => void;
style?: CSSProperties;
className?: string;
}
```

### Data attributes

The wrapper element exposes these attributes so you can drive CSS without re-rendering:

| Attribute | Values |
|-----------|--------|
| `data-edge` | `left`, `right`, `top`, `bottom` |
| `data-orientation` | `vertical`, `horizontal` |
| `data-dragging` | present while the user is actively dragging |

### CSS classes

| Class | When |
|-------|------|
| `snap-dock` | Always present |
| `snap-dock--dragging` | While the user is actively dragging |

---

## Use Cases

- **Chat widgets** — floating support/chat buttons that stay accessible
- **Chat widgets** — floating support buttons that stay accessible
- **Floating toolbars** — draggable formatting bars or quick-action panels
- **Debug panels** — dev tools overlays that can be moved out of the way
- **Side docks** — VS Code / Figma-style side rails that snap to any edge
- **Debug panels** — dev tool overlays that can be moved out of the way
- **Media controls** — picture-in-picture style video or audio controls
- **Notification centers** — persistent notification panels users can reposition
- **Accessibility helpers** — movable assistive overlays

## How It Works
## How it works

Under the hood both components use the [Pointer Events API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) for universal input handling and a `ResizeObserver` to stay pinned when their content changes size. They render as `position: fixed` elements at the top of the z-index stack (`2147483647`), so they float above everything without interfering with your layout.

Under the hood, react-driftkit uses the [Pointer Events API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) for universal input handling and a `ResizeObserver` to keep the widget positioned correctly when its content changes size. The widget renders as a `position: fixed` div at the highest possible z-index (`2147483647`), so it floats above everything without interfering with your layout.
`SnapDock`'s orientation flip uses a FLIP-style animation: it captures the old wrapper rect before the orientation changes, applies an inverse `scale()` anchored to the active edge, and animates back to identity in the next frame — so the dock glides between horizontal and vertical layouts instead of snapping.

## Contributing

Contributions are welcome! Feel free to open an issue or submit a pull request.
Contributions are welcome. Open an issue or send a pull request.

```bash
git clone https://github.com/shakcho/react-drift.git
Expand Down
35 changes: 27 additions & 8 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,38 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>react-driftkit — Draggable floating widgets for React</title>
<meta name="description" content="A draggable, floating widget wrapper for React — snap to corners or drag anywhere. Lightweight, zero-dependency, and fully customizable." />
<title>react-driftkit - Floating UI primitives for React</title>
<meta name="description" content="Small, tree-shakable React components for floating UI: draggable launchers, chat widgets, toolbars, and edge-pinned docks. Unstyled, TypeScript-first, React 18 and 19." />
<link rel="canonical" href="https://react-driftkit.saktichourasia.dev/" />
<link rel="alternate" type="text/plain" href="/llms.txt" title="llms.txt" />

<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content="react-driftkit — Draggable floating widgets for React" />
<meta property="og:description" content="A draggable, floating widget wrapper for React — snap to corners or drag anywhere. Lightweight, zero-dependency, and fully customizable." />
<meta property="og:url" content="https://github.com/shakcho/react-drift" />
<meta property="og:title" content="react-driftkit - Floating UI primitives for React" />
<meta property="og:description" content="Small, tree-shakable React components for floating UI: draggable launchers, chat widgets, toolbars, and edge-pinned docks. Unstyled, TypeScript-first, React 18 and 19." />
<meta property="og:url" content="https://react-driftkit.saktichourasia.dev/" />

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="react-driftkit — Draggable floating widgets for React" />
<meta name="twitter:description" content="A draggable, floating widget wrapper for React — snap to corners or drag anywhere. Lightweight, zero-dependency, and fully customizable." />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="react-driftkit - Floating UI primitives for React" />
<meta name="twitter:description" content="Small, tree-shakable React components for floating UI: draggable launchers, chat widgets, toolbars, and edge-pinned docks. Unstyled, TypeScript-first, React 18 and 19." />

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareSourceCode",
"name": "react-driftkit",
"description": "Small React primitives for floating UI: draggable launchers and edge-pinned docks.",
"codeRepository": "https://github.com/shakcho/react-drift",
"programmingLanguage": "TypeScript",
"runtimePlatform": "React 18, React 19",
"license": "https://github.com/shakcho/react-drift/blob/main/LICENSE",
"url": "https://react-driftkit.saktichourasia.dev/",
"sameAs": [
"https://www.npmjs.com/package/react-driftkit"
]
}
</script>

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
Expand Down
Loading
Loading