npm install react-driftkitimport { MarqueeSelect } from 'react-driftkit';behavior.startOnItem opts back in. Nothing here re-renders on hover: the dashed outline is pure CSS off the data-marquee-hit attribute the component writes onto each node.| Prop | Type | Default |
|---|---|---|
children The selectable surface — a canvas, board, or grid. Rendered inside a position: relative root. | ReactNode | — |
itemSelector CSS selector matching the selectable items, queried inside the root. Selector-based rather than registration-based, so virtualised lists and third-party children work unchanged. | string | '[data-marquee-item]' |
selection Controlled selection. When provided, the component renders exactly what you pass and nothing moves until you apply on.select. | string[] | — |
defaultSelection Starting selection in uncontrolled mode. | string[] | [] |
additive Treat an unmodified drag as add instead of replace, so every drag extends the selection. | boolean | false |
disabled Turns the gesture off without unmounting. An in-flight drag is rolled back immediately. | boolean | false |
behavior Gesture tuning — hit testing, what starts a drag, identity, and auto-scroll. | MarqueeSelectBehavior | — |
behavior.hitTest intersect catches anything the box touches; contain only catches items the box fully encloses. | 'intersect' | 'contain' | 'intersect' |
behavior.threshold Pointer travel in px before a press counts as a marquee. Below it, the press is an empty-space click instead. | number | 4 |
behavior.startOnItem Allow a marquee to start on top of an item. Off by default so pressing an item leaves the gesture to whatever drag logic that item owns. | boolean | false |
behavior.ignoreSelector Elements whose press is left entirely alone, so buttons, links, and text fields inside the surface keep working. | string | form controls, a[href], editable content, [data-marquee-ignore] |
behavior.clearOnEmptyClick A press that never crosses threshold clears the selection. Skipped when a modifier is held, so shift-clicking empty space does not wipe your work. | boolean | true |
behavior.markItems Mirror state onto the item elements as data-marquee-selected and data-marquee-hit, so CSS can style them without a re-render. Marks are removed on unmount. | boolean | true |
behavior.autoScroll Scroll the root when the pointer nears its edge during a drag. Requires the root to actually scroll — put overflow: auto and a height on it via style. | boolean | true |
behavior.autoScrollMargin Distance in px from the edge where auto-scroll begins. | number | 48 |
behavior.autoScrollSpeed Peak auto-scroll speed in px per frame, reached at the very edge. It ramps up as the pointer gets closer. | number | 14 |
behavior.touch Let touch pointers start a marquee. Off by default so a touch drag still pans the board; turning it on sets touch-action: none on the root. | boolean | false |
behavior.mouseButton Mouse button that starts a marquee. Set it to 1 or 2 to reserve the primary button for something else. | number | 0 |
behavior.getItemId Identity for an item element. Use it when ids already live somewhere else in your markup. | (element: HTMLElement, index: number) => string | data-marquee-id → id → index |
behavior.resolveMode Replaces the whole modifier-key mapping. This is the only route to toggle mode, which flips each caught item in and out of the selection. | (event: PointerEvent) => MarqueeMode | shift/ctrl/meta → add, alt → subtract |
on Event handlers: select, start, end, cancel. All optional. | MarqueeSelectEvents | — |
on.select Fires whenever the effective selection changes, live during the drag. Skipped when a move leaves the result unchanged, so it is safe to wire straight to a state setter. | (ids: string[], info: MarqueeSelectInfo) => void | — |
on.start Fires once the press has crossed threshold and the box has appeared — not on every press. | (info: MarqueeSelectInfo) => void | — |
on.end Fires on release with the committed selection. Use it when the expensive work should only run once, not on every frame. | (ids: string[], info: MarqueeSelectInfo) => void | — |
on.cancel Fires when Escape or a pointer cancel aborts the drag. The selection is rolled back to its pre-drag value first, and on.end does not fire. | (info: MarqueeSelectInfo) => void | — |
marquee Replaces the default box entirely — render a count badge, a snapped grid outline, whatever you like. Only mounted while a drag is live. | (state: MarqueeSelectState) => ReactNode | — |
className CSS class added to the root container. | string | '' |
style Inline styles merged onto the root container. Put overflow: auto and a height here for a scrolling board with edge auto-scroll. | CSSProperties | — |
marqueeClassName CSS class added to the default marquee box. | string | '' |
marqueeStyle Inline styles merged onto the default marquee box. Set a zIndex here if your items create stacking contexts of their own. | CSSProperties | — |
Item boxes are measured once per drag, at pointer-down, and stored in container content coordinates — client pixels minus the root's padding-box origin, plus its scroll offset. That makes every subsequent move a plain rectangle overlap test with no DOM reads at all, and it is what keeps the box glued to the point you pressed while the edges auto-scroll under it. The trade-off is that items which move during a drag are hit-tested against where they were when it started. The pre-drag selection is kept separately from the live hit set and the mode is re-applied from scratch on every frame, which is why dragging back out of an area cleanly undoes itself and why Escape can roll the whole drag back in one step. The root exposes data-marquee-root, data-marquee-active, and data-marquee-mode; the items get data-marquee-selected and data-marquee-hit. Like the other drag primitives here, the gesture is not keyboard-reachable — pair it with checkboxes or a Select all control when selection is the only route to an action.
import { useState } from 'react';
import { MarqueeSelect } from 'react-driftkit';
function Board({ nodes }) {
const [selected, setSelected] = useState<string[]>([]);
return (
<MarqueeSelect
selection={selected}
on={{ select: setSelected }}
style={{ height: 600, overflow: 'auto' }}
>
{nodes.map((node) => (
<div
key={node.id}
data-marquee-item
data-marquee-id={node.id}
className="node"
style={{ position: 'absolute', left: node.x, top: node.y }}
>
{node.label}
</div>
))}
</MarqueeSelect>
);
}type MarqueeHitTest = 'intersect' | 'contain';
type MarqueeMode = 'replace' | 'add' | 'subtract' | 'toggle';
// A box in container content coordinates — the root's padding-box
// origin, with its scroll offset already folded in.
interface MarqueeRect {
x: number;
y: number;
width: number;
height: number;
}
interface MarqueeSelectState {
active: boolean; // true from threshold crossing until release
rect: MarqueeRect | null;
ids: string[]; // the effective selection, live
hit: string[]; // caught by the box, before the mode applies
mode: MarqueeMode;
}
interface MarqueeSelectInfo {
rect: MarqueeRect | null;
mode: MarqueeMode;
hit: string[];
base: string[]; // the selection as it was when the drag began
elements: HTMLElement[];
}
interface MarqueeSelectProps {
children?: ReactNode;
itemSelector?: string; // default '[data-marquee-item]'
selection?: string[]; // controlled
defaultSelection?: string[]; // uncontrolled, default []
additive?: boolean; // default false
disabled?: boolean; // default false
behavior?: {
hitTest?: MarqueeHitTest; // default 'intersect'
threshold?: number; // default 4 — px before it is a drag
startOnItem?: boolean; // default false
ignoreSelector?: string; // form controls, links, editable content
clearOnEmptyClick?: boolean; // default true
markItems?: boolean; // default true — data-attrs on the items
autoScroll?: boolean; // default true
autoScrollMargin?: number; // default 48 (px from the edge)
autoScrollSpeed?: number; // default 14 (px per frame at the edge)
touch?: boolean; // default false — touch pans instead
mouseButton?: number; // default 0
getItemId?: (element: HTMLElement, index: number) => string;
resolveMode?: (event: PointerEvent) => MarqueeMode;
};
on?: {
select?: (ids: string[], info: MarqueeSelectInfo) => void;
start?: (info: MarqueeSelectInfo) => void;
end?: (ids: string[], info: MarqueeSelectInfo) => void;
cancel?: (info: MarqueeSelectInfo) => void;
};
// Replaces the default box. Rendered only while active.
marquee?: (state: MarqueeSelectState) => ReactNode;
className?: string;
style?: CSSProperties;
marqueeClassName?: string;
marqueeStyle?: CSSProperties;
}Enjoying react-driftkit?
Star the repo on GitHub to help more devs discover it.