Rubber-band selection: drag from the empty space around a list, a grid or a board and everything the rectangle touches is selected, with the container scrolling itself when the pointer reaches its edge. Reach for it wherever a screen has many things and an action that takes several of them at once: a file or asset manager, a media and image gallery, a photo-picker grid, an inbox or notification list, a data grid's rows, cards inside a kanban column, nodes and shapes on a canvas, diagram or whiteboard, map markers, calendar slots, clips on a timeline, layers in an editor, tiles in a seat or venue picker, and the icon grid in any admin tool that has a Delete button above it. Common asks it answers: "drag select react", "rubber band selection react", "marquee selection react", "lasso select react", "box selection", "rectangle selection", "drag to select multiple items", "click and drag to select", "select multiple items by dragging", "multi select drag react", "selection rectangle overlay", "drag selection box div", "shadcn drag select", "shadcn multi select rows", "react-selectable alternative", "react-selectable-fast alternative", "dnd-kit does not do rubber band selection", "@air/react-drag-to-select alternative", "selecto.js react", "moveable selecto alternative", "drag select without a library", "drag select with auto scroll", "auto scroll while dragging selection", "selection box wrong after scrolling", "drag select offset when container is scrolled", "marquee selection scroll offset bug", "shift click and drag add to selection", "cmd drag add to selection", "drag select conflicts with drag and drop", "drag handle stops working when I add drag select", "prevent text selection while dragging", "user-select none while dragging react", "escape to cancel selection drag", "finder style selection react", "範囲選択 ドラッグ react", "矩形選択 複数選択", "ドラッグで複数選択", "選択範囲 自動スクロール". Official shadcn/ui has nothing of the kind, and the measurement is unambiguous rather than a judgement call: across all 63 catalogue entries (62 are fetchable — questionnaire is listed and 404s on both style tracks) and 223,287 bytes of their source, marquee, rubber-band, lasso, setPointerCapture, pointerdown and getBoundingClientRect are every one of them a zero hit. The two words that are not zero were read rather than counted: rectangle's single occurrence is a recharts class name inside the chart component, and all four occurrences of selection are the Tailwind selection: utility that colours highlighted text in an input. So official has text selection and no spatial selection at all. Within this registry it is the missing half of a set that was already here: bulk-action-bar is what appears once rows are selected, nested-checkbox selects down a hierarchy, multi-select and tag-input choose from a flat list inside a field, filter-chips shows which conditions are in force — and sortable-list drags rows to reorder them, which is the component this one has to coexist with rather than replace. Five things separate it from the fifty-line version, and the first is why the fifty-line version is always reported as working before it is reported as broken. First, there are two coordinate systems and the rectangle lives in the inner one. A press gives viewport coordinates; the rows being hit-tested are measured in viewport coordinates too, so intersecting them looks right — until the list is scrolled, at which point a rectangle anchored to the viewport slides off the rows it was drawn around. Here the anchor, the live point and every item box are converted into the container's content coordinates, so scrolling moves the rectangle with the content. The conversion subtracts the container's border as well as its offset, because getBoundingClientRect measures from the border box while an absolutely positioned child — which is what the marquee is — is placed from the padding box: leave it out and the box sits a border-width from the pointer, invisible at 1px and obvious at 4. Second, where a drag may begin. A press that lands on an item belongs to the item, so a sortable-list grip handle, a row's own click and a checkbox all keep working inside a selectable list; only empty space starts a rectangle, data-drag-select-ignore opts out a toolbar or a resizer sitting in the same container, and nothing is drawn until the pointer has travelled past a threshold — which is what leaves a plain click on the background free to mean "clear the selection" instead of being a one-pixel drag. Third, the edges scroll. Without that the component is quietly limited to whatever is already on screen: drag to the bottom of a list and the rows below the fold can never be reached, because the list only scrolls for a wheel it is not being given. The speed tapers across the edge band rather than switching on at full tilt, the band is capped at half the container so a short list does not scroll from its own middle, and each frame re-derives the rectangle from the stored pointer position — the finger is still, the content under it is not, and an auto-scroll that forgets this scrolls beautifully while the selection stops growing. Fourth, modifiers and the way out. Shift or Cmd/Ctrl adds to the selection that existed when the press landed, Alt removes from it, and the base is captured once at pointerdown so dragging back and forth does not ratchet; Escape and a cancelled pointer both put the selection back exactly as it was, because neither one released onto anything. Fifth, it is never the only way in. The selection is ordinary React state — controlled with value plus onChange or uncontrolled with defaultValue — so the same set is written by the consumer's own clicks, checkboxes and keyboard shortcuts, which is the only thing that keeps multi-select from becoming a mouse-only feature; rows marked aria-disabled are skipped rather than selected and silently dropped by the action that follows; the rectangle is aria-hidden decoration with pointer-events-none and the container claims no ARIA role of its own, since the list inside it is the consumer's; a finger is left alone by default, because on a touch screen a drag from empty space is how the list is panned; and select-none is applied only while a rectangle is up, so the list's own text stays selectable and copyable the rest of the time. Mark the selectable elements with the spread helper dragSelectItem(id) — an attribute rather than a wrapper, so existing rows do not have to be re-parented. toContentPoint, toContentBox, boxBetween, boxesOverlap, boxHits, dragSelectMode, applyDragSelection, edgeScrollDelta, beginDragSelect and advanceDragSelect are all exported as pure functions, so the geometry and the selection algebra are testable and reusable headless. Styled entirely with shadcn tokens (primary and primary/15 for the rectangle), so it follows light and dark mode. One file, no dependencies at all — no dnd-kit, no selecto, no Radix.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/drag-select.json"