Skip to content
caliper ui docs

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.

Free
pnpm dlx shadcn@latest add @caliper/marquee-select

Trip photos

Drag across photos to select them; Shift adds.

Dunes at dusk
Barley field
Forest path
Harbour
Night street
Ridge line
Terrace
Harbour, second take
1 selected
Installs components/ui/marquee-select.tsxDepends on none beyond ReactMotion none in component source

When 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

terminal
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.

example.tsx
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.

PropTypeDefaultDescription
label *string—Names the collection, e.g. "Photos".
items *MarqueeItem[]—
valuestring[]—
defaultValuestring[][]
onValueChange((value: string[]) => void)—
gridClassNamestring—Classes for the grid of items, such as its columns.

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.

KeyBehaviour
→Moves to the next item or value.
←Moves to the previous item or value.
↓Moves to the next item or value.
↑Moves to the previous item or value.
HomeJumps to the first item or the minimum.
EndJumps to the last item or the maximum.
SpaceActivates the focused control.
EscapeCloses or cancels, then returns focus to where it came from.

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.

All Compare & browse examples