# Selection

Choose items, extend a selection, or select a rectangle of shapes or cells.

`createSelection` chooses item keys and extends ranges, `createLasso` selects items with a drawn rectangle, and `createCellRange` selects a block of grid cells.

Import them from `@ui-sdk/core`. Pair them with [Focus and Keys](/docs/core/behaviors/focus) to move between items and choose them.

## Selecting Items

### Choose One

Keep the selected keys in a `string[]`, even when only one item can be chosen. In `api`, add `createSelection` alongside the existing roving focus behavior:

```ts
const change = createChange(context)
const selection = createSelection(context, {
  key: "value",
  set: (value, details) => change("value", value, details),
  order: () => roving.items().map((item) => item.key),
})
```

Start `value` at `[]` and return `selection` from `api`. The `set` callback uses the same [change pattern](/docs/core/kernel/handle-changes#changes) as the guides.

The behavior gives you methods to call from your event handlers. For example, a pointer press can choose one item:

```ts
pointerdown: (event, { api, key }) => api.selection.choose(key, { reason: "item-press", event }),
```

`choose` selects that item. `toggle` also lets the user clear it. Add this branch to your existing key handler for Space:

```ts
if (event.key === " ") {
  event.preventDefault()
  selection.toggle(key, { reason: "item-press", event })
}
```

This plan picker uses both. `selection.isSelected(key)` lets its Enter handler check whether a plan is chosen before continuing.

Click a plan, or use the arrows and Space to choose one. Space on the chosen plan clears it; Enter continues with it, and the console logs each step.

For a listbox, give the root `role="listbox"`, each item `role="option"`, and set its `aria-selected` from `state.value.includes(key)`. Keep the focus wiring from [Focus and Keys](/docs/core/behaviors/focus#roving-focus).

### Choose Many

Add `multiple` to the selection options:

```ts
multiple: () => true,
```

Now `toggle` adds or removes one item while keeping the others. Use it for both pointer presses and Space, as in this topping picker. A listbox that allows this also needs `aria-multiselectable="true"` on its root.

Click toppings to add or remove them. With the keyboard, use Left and Right, then Space.

### Ranges and Select All

`extend` selects from an anchor to another item. The anchor is the last item chosen or toggled without Shift. These three calls give a file list its usual pointer interactions:

```ts
press(event: PointerEvent, key: string) {
  const details = { reason: "item-press" as const, event }
  if (event.shiftKey) {
    selection.extend(key, { ...details, from: context.store.get().focused })
  } else if (event.metaKey || event.ctrlKey) {
    selection.toggle(key, details)
  } else {
    selection.choose(key, details)
  }
},
```

For Shift with the arrows, use the result of the existing focus handler:

```ts
const moved = roving.keydown(event, el)
if (moved?.shift) {
  selection.extend(moved.key, {
    from: moved.from,
    reason: "list-navigation",
    event,
  })
}
```

`from` supplies the starting point when there isn't an anchor yet. `selection.anchor()` returns the current anchor's key.

A range keeps what was selected when the anchor was set, then adds the items between the anchor and the target. Pass `additive: false` to select only the range. Supplying a different selection from outside starts the next range over.

Click a file, then Shift-click another for a range. Control or Command-click toggles one file. Turn on Ranges replace the selection to compare.

In multiple mode, `toggleAll` selects every available key in `order`, or clears them if they're already selected. Call it from your Control or Command+A handler after preventing the browser's default action:

```ts
selection.toggleAll({ reason: "list-navigation", event })
```

`order` determines what ranges and select-all can reach. For a virtualized list, pass the full list of keys, including items that aren't rendered.

### Items That Can't Be Selected

Use `selectable` to skip keys that shouldn't be chosen, toggled, or added to a range:

```ts
selectable: (key) => !context.options().owners.includes(key),
```

In this member list, owners start selected. The demo uses `toggle` and `toggleAll`, which leave those owners alone.

Focus a member and press Control or Command+A twice. Hollis Grant stays selected while the other members are selected, then cleared.

This doesn't pin an item in the selection: `choose` and a range with `additive: false` can replace the whole value. If some keys must always stay selected, enforce that in your `set` callback.

## Rectangle Selection

### Draw a Rectangle

`createLasso` selects items touched by a rectangle. Start with `selected: string[]` and `lasso: LassoDraft | null` in state, with `lasso` set to `null`. Add `lasso` to the primitive's `live` fields, as covered in [Live State](/docs/core/kernel/handle-changes#live-state).

Create the behavior in `api` and return it as `lasso`:

```ts
const lasso = createLasso(context, {
  slot: "shape",
  draft: "lasso",
  selected: () => context.store.get().selected,
  set: (keys, details) => change("selected", keys, details),
})
```

Give the surface `position: relative`, `touch-action: none` and `user-select: none` so drawing doesn't scroll the page or select text. Start when the pointer lands on its empty space:

```ts
pointerdown: (event, { el, api }) => {
  if (el && event.target === el) {
    api.lasso.pointerdown(event, el)
  }
},
```

Keep the rectangle part rendered. Its `style` hides it when there's no drag:

```ts
lasso: slot({
  attrs: () => ({ "aria-hidden": "true" }),
  live: live((state, { api }) => ({ style: api.lasso.style(state) })),
}),
```

Drag from empty space to select shapes; the console logs the selection. Hold Shift to add to it; press Escape before releasing to cancel the rectangle.

`lasso.touches(state, key)` tells you which items belong to the preview selection. With Shift, that includes items selected before the drag. Use it for a `data-lassoed` highlight.

The selection changes once, when the pointer is released, with the reason `"lasso"`. Escape cancels the drag and keeps the previous selection.

### Items It Skips

Add `selectable` to exclude items from the rectangle's hits, and `message` to customize the screen reader announcement:

```ts
selectable: (key) => !context.options().locked.includes(key),
message: (count) => `${count} ${count === 1 ? "photo" : "photos"} selected`,
```

The announcement runs after the selection request is accepted.

Drag from empty space across the photos. City and Lake stay unselected; a screen reader announces the selected count when you release.

## Ranges of Cells

### Select a Block

[Focus and Keys](/docs/core/behaviors/focus#grids) shows how to navigate a grid. Add `createCellRange` when you also need to drag across cells, select whole rows or columns, or copy a block.

Keep a `range: CellRange | null` field in state, starting at `null`. Pass the existing grid behavior and its row and column counts:

```ts
const range = createCellRange(context, {
  key: "range",
  grid,
  rows: () => 4,
  cols: () => 5,
})
```

Return `range` from `api`, then update the existing cell slot:

```ts
selects: selects({ active: byCell, range: byCellRange }),
attrs: (state, { props, api }) => ({
  role: "gridcell",
  tabindex: api.grid.tabIndex(state, props),
  ...api.range.cellAttrs(state, props),
}),
on: {
  keydown: (event, { api }) => api.range.keydown(event),
  pointerdown: (event, { api, props }) => api.range.pointerdown(event, props),
},
bind: (el, { api, props }) => api.range.bind(el, props),
```

`range.bind` also binds grid focus, and `range.keydown` forwards ordinary navigation to the grid. You don't need to call both behaviors for the same event. `byCellRange` lets a range change reach only the cells that join or leave it and the cells along the sides that move.

Drag across cells, or click one and hold Shift with the arrows. The total follows the selected range.

Focus stays on the active cell as the range grows. A move without Shift clears the range. The behavior also handles these shortcuts:

| Keys          | Action                   |
| ------------- | ------------------------ |
| Shift+Space   | Select the range's rows. |
| Control+Space | Select its columns.      |
| Control+A     | Select every cell.       |
| Escape        | Clear the range.         |

`cellAttrs` marks the explicit range with `aria-selected`, `data-selected`, and `data-range-edge`. The last one lists the sides of the range a cell sits on, such as `"top left"`, so you can draw an outline with `[data-range-edge~="top"]`.

### Selecting from Code

The same range can be changed from buttons or other controls. Row and column indexes start at zero:

```ts
range.selectColumns(1)
range.selectRows(0, 1)
range.select({ anchor: { row: 1, col: 1 }, end: { row: 2, col: 2 } })
range.selectAll()
range.select(null)
```

These calls change the range without moving focus. `select(null)` clears it.

Use the buttons to select a column, rows, a block or the whole grid. The summary names the range and counts values over 10.

`range.bounds(state)` gives the inclusive `top`, `left`, `bottom` and `right` indexes. With no explicit range, it falls back to the active cell, or returns `null` if there isn't one. `range.contains(state, cell)` uses the same rule when you need to total or count cells.

By default, range changes write to state directly. To report them through `createChange`, add `set`. You can also customize what screen readers hear after keyboard selection:

```ts
set: (value, details) => change("range", value, details),
message: ({ top, left, bottom, right }) =>
  `Rows ${top + 1} to ${bottom + 1}, columns ${left + 1} to ${right + 1} selected`,
```

For rows that aren't rendered yet, `reveal` works as it does in [grid focus](/docs/core/behaviors/focus#rows-that-arent-rendered).

### Copy and Paste

`range.text(state, read)` reads the selected cells as tab-separated text. Call it from the grid's `copy` handler:

```ts
copy(event: ClipboardEvent) {
  const state = context.store.get()
  const text = range.text(state, ({ row, col }) => state.cells[row][col])
  event.preventDefault()
  event.clipboardData?.setData("text/plain", text)
},
```

In the `paste` handler, `textToCells` turns clipboard text into rows:

```ts
const rows = textToCells(event.clipboardData?.getData("text/plain") ?? "")
```

Your handler prevents the default paste and writes those values into state, starting at the selection's top-left cell. Stop at the grid's edges. The demo's Code tab shows the full handler.

Select cells and copy with Control or Command+C. Click a destination cell and paste with Control or Command+V.

`cellsToText(rows)` works in the other direction. Use it to copy an entire sheet, even when only part of it is selected.

## API Reference

### Item Selection Options

- `key` (`Key`): The state key that holds the selected keys, a `string[]`. Example: [Choose One](#choose-one).
- `set` (`(value: string[], details: Why<Reason>) => boolean`): Asks for a new selection and returns whether it went through. Pass the primitive's change, so `on<Key>Change`, `cancel()` and controlled values work: `set: (value, details) => change("value", value, details)`. Example: [Choose One](#choose-one).
- `order` (`() => readonly string[]`): Every key in list order, for ranges and select all, like the visible items' keys. Example: [Choose One](#choose-one).
- `multiple?` (`(() => boolean)`): Whether more than one item can be selected. Defaults to `false`. Example: [Choose Many](#choose-many).
- `selectable?` (`((key: string) => boolean)`): Whether a key can be chosen, toggled, or added to a range. Replacing the selection can still remove it. Defaults to every item. Example: [Items That Can't Be Selected](#items-that-cant-be-selected).

### Rectangle Selection Options

- `slot` (`string`): The slot whose parts the rectangle selects. Example: [Draw a Rectangle](#draw-a-rectangle).
- `draft` (`keyof State & string`): The state key that holds the `LassoDraft` while one is drawn. Start it at `null`, and list it in `live`. Example: [Draw a Rectangle](#draw-a-rectangle).
- `selected` (`() => string[]`): The selection now, which Shift adds to. Example: [Draw a Rectangle](#draw-a-rectangle).
- `set` (`(keys: string[], details: { reason: "lasso"; event?: Event; }) => boolean`): Asks for a new selection. Pass the primitive's change. Example: [Draw a Rectangle](#draw-a-rectangle).
- `selectable?` (`((key: string) => boolean)`): Whether an item can be selected. Defaults to every item. Example: [Items It Skips](#items-it-skips).
- `message?` (`((count: number) => string)`): What screen readers hear when the rectangle lifts. Defaults to English. Example: [Items It Skips](#items-it-skips).

### Cell Range Options

- `key` (`keyof State & string`): The state key that holds the `CellRange`, or `null` when only the active cell is selected. Example: [Select a Block](#select-a-block).
- `grid` (`object`): The grid's focus, which keeps the active cell. Example: [Select a Block](#select-a-block).
- `rows` (`() => number`): How many rows the range can reach. Example: [Select a Block](#select-a-block).
- `cols` (`() => number`): How many columns. Example: [Select a Block](#select-a-block).
- `reveal?` (`((cell: GridCell) => void)`): Shows a cell the range grows to that may not be rendered, like `createGridFocus`'s `reveal`. Example: [Rows That Aren't Rendered](/docs/core/behaviors/focus#rows-that-arent-rendered).
- `set?` (`((range: CellRange | null, details: { reason: CellRangeReason; event?: Event; }) => boolean)`): Asks for a new range and returns whether it went through. Defaults to writing the state; pass the primitive's change to report it. Example: [Selecting from Code](#selecting-from-code).
- `message?` (`((bounds: CellBounds) => string)`): What screen readers hear after the range changes from the keyboard. Defaults to English. Example: [Selecting from Code](#selecting-from-code).
