A small React drag-selection library with mouse, pen and touch input, frame-batched updates, and continuous edge scrolling. React is its only runtime dependency.
npm install @badasukerubin/react-simple-drag-selectionSupports React 16.8, 17, 18 and 19 in browsers with Pointer Events, DOMRect and requestAnimationFrame. Importing and rendering on the server does not access the DOM; gestures attach after mounting.
import { useContext, useRef } from "react";
import {
DragSelectionContext,
SelectionBox,
useDragSelected,
useDragSelection,
} from "@badasukerubin/react-simple-drag-selection";
function Item() {
const elementRef = useRef<HTMLDivElement>(null);
const selection = useContext(DragSelectionContext);
const selected = useDragSelected({ elementRef, selection });
return (
<div ref={elementRef} style={{ background: selected ? "#aac4ff" : "#eee" }}>
Select me
</div>
);
}
export function Example() {
const containerRef = useRef<HTMLDivElement>(null);
const boxRef = useRef<HTMLDivElement>(null);
const selection = useDragSelection({ containerRef, boxRef });
return (
<DragSelectionContext.Provider value={selection}>
<div ref={containerRef} style={{ height: 300, overflow: "auto" }}>
<SelectionBox ref={boxRef} />
<Item />
</div>
</DragSelectionContext.Provider>
);
}Render SelectionBox in the same document, typically beside the selectable content or in a React portal. React retains ownership of the element throughout the gesture. Keep layout styles on the container and visual styles on the box. Geometry, display, pointer events and positioning are managed by the hook.
Returns a viewport-coordinate DOMRect | null, matching getBoundingClientRect().
| Option | Default | Purpose |
|---|---|---|
containerRef |
Required | Selection surface, including its scrollable content. Nullable refs are supported. |
scrollContainerRef |
None | Optional scrolling ancestor when the selection surface does not scroll itself. |
boxRef |
Required | Ref to SelectionBox or an equivalent div in the same document. |
mouseMoveThreshold |
5 |
Movement must exceed this distance on either axis before drawing. Applies to mouse, pen and touch. |
disabled |
false |
Disable gestures and clear the rectangle. |
touch |
true |
Enable touch selection and temporarily apply touch-action: none to the container. Use false to retain native touch scrolling. |
autoScroll |
true |
Scroll the container near its edges. Use false or { edgeSize: 24, speed: 600 }. Distance is in CSS pixels; speed is pixels per second. |
ignoreSelector |
None | Additional valid CSS selector for targets that cannot start a gesture. |
shouldStartSelection |
None | Return false to reject a press. Receives its PointerEvent, including modifier keys. |
onSelectionStart |
None | Called once on an accepted press, before crossing the threshold. Receives the PointerEvent. |
onSelection |
None | Receives each changed rectangle, at most once per animation frame, plus a final flush on release when needed. |
onSelectionEnd |
None | Receives { selection, cancelled, event } on release or cancellation. |
A successful release hides the box and retains the final rectangle. The next accepted press clears it. An ordinary click below the movement threshold keeps native click behavior. Escape, pointer cancellation, capture loss, a second pointer, window blur, page hiding, or native HTML dragging cancels the gesture and restores the previous rectangle. Disabling, replacing refs or unmounting tears down the gesture without calling application callbacks.
Callbacks use the latest committed props. Movement and scroll events share one animation frame. Auto-scroll continues while the pointer rests at an edge and stops at the scroll limit; it scrolls only the configured container (or its explicit scrollContainerRef ancestor) and its enabled overflow axes. Ancestor/window scrolling updates the anchor. Scroll offsets, borders, translation and positive axis-aligned scaling are accounted for.
Inputs, textareas, selects, buttons, links, labels, summaries, editable content and native draggable elements cannot start selection. Descendants of .ignore-drag-selection and [data-ignore-drag-selection] are also excluded, including SVG targets and open shadow DOM event paths. Nested selection surfaces give the gesture to the innermost accepting surface.
<div className="ignore-drag-selection">
<div draggable onDragStart={startNativeDrag}>
Drag this item
</div>
</div>Only the click belonging to a completed drag is suppressed. Keyboard clicks, later pointer presses and unrelated controls retain their normal behavior.
The library measures a rectangle; your application owns item identities, keyboard navigation, additive selection and range selection. For example, use shouldStartSelection: event => !event.altKey to reserve Alt-drag for another interaction. Read shiftKey, ctrlKey and metaKey in onSelectionStart to choose how your application combines the gesture's matches with its existing selection. Use buttons or another keyboard-accessible control for non-pointer selection; drag selection must not be the only way to select an item.
| Option | Default | Purpose |
|---|---|---|
elementRef |
Required | Nullable ref to the item element. Read after the DOM commit. |
selection |
Required | Rectangle from the hook/context, or null. |
collision |
"intersect" |
Use "contain" to require the whole item inside the rectangle. |
onSelected |
None | Receives the new boolean only when membership changes, including deselection. |
Returns a boolean. Hidden, zero-size elements and zero-area selection rectangles do not match. Touching rectangle edges count as intersection. Keep the item ref on the element representing the actual selectable area.
Virtualized lists can only measure mounted items. Store selected IDs in your application when selecting across virtual pages, and use your list's geometry/model for items that are not mounted. A retained rectangle is geometry, not a persistent ID-based selection. Rotated/skewed coordinate systems and cross-origin iframe gestures are outside the rectangular hit-test model. For unclipped overlays inside CSS paint containment or stacking contexts, render the box through a React portal into that document's body.
Accepts normal div props and a forwarded ref. Defaults to a hidden, fixed, non-interactive, aria-hidden rectangle. Customize className, background, border and z-index; keep the hook's positioning and sizing properties free of CSS !important overrides.
The host supplies its resolved "light" or "dark" theme. Both use neutral gray: a dark fill/outline on light surfaces and a light fill/outline on dark surfaces. Omitting theme preserves the original light appearance. The package does not read system preferences, persist a theme, or add a theme provider.
<SelectionBox ref={boxRef} theme={isDark ? "dark" : "light"} />
// Optional custom colors, independent of the theme:
<SelectionBox
ref={boxRef}
theme="dark"
backgroundColor="rgba(59, 130, 246, 0.2)"
accentColor="var(--selection-accent, #60a5fa)"
/>| Prop | Default | Purpose |
|---|---|---|
theme |
"light" |
Host-resolved appearance. Pass the resolved mode for a host's system-theme setting. |
backgroundColor |
Theme's translucent neutral | Fill color. Supply alpha for transparency; transparent disables the fill. |
accentColor |
Theme's neutral outline | Outline color; does not tint the fill or selected items. |
Color props accept CSS colors, including CSS variables. Each override is optional; removing it restores the theme default. Explicit style overrides take precedence (color also overrides the default outline's currentColor). Theme/color changes apply during a drag without resetting selection. Item highlights remain owned by your application. Public SelectionBoxProps and SelectionBoxTheme types are exported.
Use Node.js 22.12 or newer.
npm ci
npm run typecheck
npm test
npx playwright install chromium firefox webkit
npm run test:browser
npm packThe browser suite exercises the built distribution in Chromium at desktop, tablet portrait, tablet landscape and mobile sizes, plus Firefox and WebKit. Touch dragging is exercised through Chromium's real input protocol. BROWSER_CHANNEL=chrome npm run test:browser uses an installed Chrome for the Chromium projects. npm pack builds and checks types before producing the distributable tarball.
The example uses the local package via file:... Build the package first, then run npm ci and npm run dev in example. Rebuild the package when editing its source.
The original hooks, context, React peer range and final-rectangle behavior remain. Pointer Events replace the separate mouse/touch listeners. Interactive targets are ignored automatically, end callbacks only fire for accepted gestures, and onSelected reports the new value on transitions. See CHANGELOG.md for fixes.