Components
Marquee select
Select many items by dragging a rubber band over them, the way a desktop does. Chosen items lift with a tick, Shift keeps what was already chosen, and a count springs up with a way to clear.
pnpm dlx shadcn@latest add @caliper/marquee-selectWhen to use
Use it for
- Grids of files, photos and assets where people act on several at once.
- Boards and canvases with bulk actions.
Skip it when
- Lists where checkboxes are expected; use a table with selection.
- Touch-only surfaces; there a tap adds or removes and the band is not used.
Decisions
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Band after a small move
- A press only starts a band once the pointer moves a few pixels, so a plain click still chooses one item.
- Taps add on touch
- On a touch screen a tap adds or removes, since there is no Shift key.
Usage
- A multi-select list box
- Arrows move by position, so any grid works; Space chooses, Shift with an arrow extends, ⌘A or Ctrl+A takes all and Escape clears.
Installation
pnpm dlx shadcn@latest add @caliper/marquee-select
Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage
This is the code behind the preview above.
import { type ReactNode } from "react" import { MarqueeSelect, type MarqueeItem } from "@/components/ui/marquee-select" function Panel({ title, context, className = "max-w-md", children }: { title: string; context: string; className?: string; children: ReactNode }) { return ( <section className={`grid w-full gap-5 rounded-overlay border border-border bg-background p-6 shadow-resting max-sm:p-4 ${className}`}> <div className="grid gap-1"> <p className="font-medium">{title}</p> <p className="text-sm text-muted-foreground">{context}</p> </div> {children} </section> ); } const photos: MarqueeItem[] = [ ["dunes", "Dunes at dusk"], ["field", "Barley field"], ["forest", "Forest path"], ["harbour", "Harbour"], ["night", "Night street"], ["ridge", "Ridge line"], ["terrace", "Terrace"], ["harbour", "Harbour, second take"], ].map(([file, label], i) => ({ id: `photo-${i}`, label, content: ( <figure className="grid gap-1.5 p-1.5"> <img src={`/media/${file}.jpg`} alt="" draggable={false} className="aspect-[4/3] w-full rounded-[8px] object-cover" /> <figcaption className="truncate px-0.5 text-xs text-muted-foreground">{label}</figcaption> </figure> ), })); export function MarqueeSelectDemo() { return ( <Panel title="Trip photos" context="Drag across photos to select them; Shift adds." className="max-w-2xl"> <MarqueeSelect label="Trip photos" items={photos} defaultValue={["photo-1"]} gridClassName="grid-cols-2 sm:grid-cols-4" /> </Panel> ); }
API reference
MarqueeSelect
component
Also accepts the native props of its root element and a ref.
Accessibility
It exposes option and listbox semantics, with aria-label, aria-multiselectable, aria-selected, aria-hidden and aria-live kept in step with state. Changes that matter are announced through a live region.
Motion
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.






