> For AI agents: the complete documentation index is available at https://wallarm.github.io/design-system/llms.txt, the full documentation bundle is available at https://wallarm.github.io/design-system/llms-full.txt.

# Selection

Turns a set of anything — cards, rows, list items — into a selectable collection: a checkbox on each one, shift-click for a range, and a bulk-action bar that arrives once something is ticked. The selection is controlled and ordered by `items`, so what comes back follows the list rather than the order of clicks, and ids no longer present in `items` are kept rather than dropped — a selection can outlive a page change, and clearing it is the caller's decision.

[Source](https://github.com/wallarm/design-system/tree/main/packages/design-system/src/components/Selection) · [Storybook](https://wallarm.github.io/design-system/storybook/?path=/docs/data-display-selection--overview)

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Copy, Trash2 } from '@wallarm-org/design-system/icons';
import { Selection, SelectionBulkBar, SelectionItem } from '@wallarm-org/design-system/Selection';
import { VStack } from '@wallarm-org/design-system/Stack';

export const Default = () => {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <Selection
      data-testid='selection'
      items={clusters}
      getItemId={c => c.id}
      value={selected}
      onChange={setSelected}
    >
      <VStack gap={12}>
        {clusters.map(c => (
          <SelectionItem key={c.id} itemId={c.id}>
            <ClusterCard cluster={c} />
          </SelectionItem>
        ))}
      </VStack>

      <SelectionBulkBar>
        <Button variant='ghost' color='neutral-alt' onClick={() => onDuplicate(selected)}>
          <Copy /> Duplicate
        </Button>
        <Button color='brand' onClick={() => onDelete(selected)}>
          <Trash2 /> Delete
        </Button>
      </SelectionBulkBar>
    </Selection>
  );
};
```

## Usage

```tsx
import { Button } from '@wallarm-org/design-system/Button';
import { Copy, Trash2 } from '@wallarm-org/design-system/icons';
import { Selection, SelectionBulkBar, SelectionItem } from '@wallarm-org/design-system/Selection';
import { VStack } from '@wallarm-org/design-system/Stack';
```

## Examples

### With select all

`SelectionAll` is the header checkbox — indeterminate while the selection is partial, and
a press while everything is selected clears it rather than reselecting.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Trash2 } from '@wallarm-org/design-system/icons';
import { Selection, SelectionAll, SelectionBulkBar, SelectionItem } from '@wallarm-org/design-system/Selection';
import { HStack, VStack } from '@wallarm-org/design-system/Stack';
import { Text } from '@wallarm-org/design-system/Text';

export const WithSelectAll = () => {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <Selection
      data-testid='selection'
      items={clusters}
      getItemId={c => c.id}
      value={selected}
      onChange={setSelected}
    >
      <VStack gap={16}>
        <HStack gap={8} align='center'>
          <SelectionAll />
          <Text size='sm' color='secondary'>
            Select all
          </Text>
        </HStack>

        <VStack gap={12}>
          {clusters.map(c => (
            <SelectionItem key={c.id} itemId={c.id}>
              <ClusterCard cluster={c} />
            </SelectionItem>
          ))}
        </VStack>
      </VStack>

      <SelectionBulkBar>
        <Button color='brand' onClick={() => onDelete(selected)}>
          <Trash2 /> Delete
        </Button>
      </SelectionBulkBar>
    </Selection>
  );
};
```

### Grid

The same machinery over a three-column grid: `Selection` lays nothing out, so the
arrangement stays entirely yours.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Selection, SelectionBulkBar, SelectionItem } from '@wallarm-org/design-system/Selection';

export const Grid = () => {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <Selection
      data-testid='selection'
      items={clusters}
      getItemId={c => c.id}
      value={selected}
      onChange={setSelected}
    >
      <div className='grid grid-cols-3 gap-12'>
        {clusters.map(c => (
          <SelectionItem key={c.id} itemId={c.id}>
            <ClusterCard cluster={c} />
          </SelectionItem>
        ))}
      </div>

      <SelectionBulkBar>
        <Button onClick={() => onInspect(selected)}>Inspect</Button>
      </SelectionBulkBar>
    </Selection>
  );
};
```

### With disabled

A `disabled` item leaves both select-all and any shift range, so "all" means all the ones
this user is allowed to touch — and the header checkbox counts only those.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Trash2 } from '@wallarm-org/design-system/icons';
import { Selection, SelectionAll, SelectionBulkBar, SelectionItem } from '@wallarm-org/design-system/Selection';
import { HStack, VStack } from '@wallarm-org/design-system/Stack';
import { Text } from '@wallarm-org/design-system/Text';

export const WithDisabled = () => {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <Selection
      data-testid='selection'
      items={clusters}
      getItemId={c => c.id}
      value={selected}
      onChange={setSelected}
    >
      <VStack gap={8}>
        <HStack gap={8} align='center'>
          <SelectionAll />
          <Text size='sm' color='secondary'>
            Select all (locked items skipped)
          </Text>
        </HStack>

        <VStack gap={12}>
          {clusters.map(c => (
            <SelectionItem key={c.id} itemId={c.id} disabled={c.locked}>
              <ClusterCard cluster={c} />
            </SelectionItem>
          ))}
        </VStack>
      </VStack>

      <SelectionBulkBar>
        <Button color='brand' onClick={() => onDelete(selected)}>
          <Trash2 /> Delete
        </Button>
      </SelectionBulkBar>
    </Selection>
  );
};
```

### Range selection

Shift-click selects everything between the last checkbox you touched and this one,
following the order of `items` rather than the order you clicked in.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Selection, SelectionBulkBar, SelectionItem } from '@wallarm-org/design-system/Selection';
import { VStack } from '@wallarm-org/design-system/Stack';

export const RangeSelection = () => {
  const [selected, setSelected] = useState<string[]>([]);
  return (
    <Selection
      data-testid='selection'
      items={clusters}
      getItemId={c => c.id}
      value={selected}
      onChange={setSelected}
    >
      <VStack gap={12}>
        {clusters.map(c => (
          <SelectionItem key={c.id} itemId={c.id}>
            <ClusterCard cluster={c} />
          </SelectionItem>
        ))}
      </VStack>
      <SelectionBulkBar>
        <Button color='brand' onClick={() => onConfirm(selected)}>
          Confirm
        </Button>
      </SelectionBulkBar>
    </Selection>
  );
};
```

### Bulk actions

Three actions in one bar. Only the destructive one is solid, so a bar with several verbs
still has a single obvious primary.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Copy, Folder, Trash2 } from '@wallarm-org/design-system/icons';
import { Selection, SelectionBulkBar, SelectionItem } from '@wallarm-org/design-system/Selection';
import { VStack } from '@wallarm-org/design-system/Stack';

export const BulkActions = () => {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <Selection
      data-testid='selection'
      items={clusters}
      getItemId={c => c.id}
      value={selected}
      onChange={setSelected}
    >
      <VStack gap={12}>
        {clusters.map(c => (
          <SelectionItem key={c.id} itemId={c.id}>
            <ClusterCard cluster={c} />
          </SelectionItem>
        ))}
      </VStack>

      <SelectionBulkBar>
        <Button variant='ghost' color='neutral-alt' onClick={() => onMove(selected)}>
          <Folder /> Move
        </Button>
        <Button variant='ghost' color='neutral-alt' onClick={() => onDuplicate(selected)}>
          <Copy /> Duplicate
        </Button>
        <Button color='brand' onClick={() => onDelete(selected)}>
          <Trash2 /> Delete
        </Button>
      </SelectionBulkBar>
    </Selection>
  );
};
```

### Empty and partial

Emptying the list strands the ids that were selected, because the component keeps them on
purpose — the effect here is the cleanup pattern, dropping any id the list no longer has.

```tsx
import { useEffect, useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Trash2 } from '@wallarm-org/design-system/icons';
import { Selection, SelectionBulkBar, SelectionItem } from '@wallarm-org/design-system/Selection';
import { HStack, VStack } from '@wallarm-org/design-system/Stack';
import { Text } from '@wallarm-org/design-system/Text';

export const EmptyAndPartial = () => {
  const [items, setItems] = useState<Cluster[]>(clusters);
  const [selected, setSelected] = useState<string[]>(['1', 'ghost']);

  // Demonstrate cleanup pattern: drop ids that no longer exist in items.
  useEffect(() => {
    setSelected(prev => prev.filter(id => items.some(c => c.id === id)));
  }, [items]);

  return (
    <VStack gap={16}>
      <HStack gap={8}>
        <Button onClick={() => setItems([])} variant='outline'>
          Clear items
        </Button>
        <Button onClick={() => setItems(clusters)} variant='outline'>
          Restore items
        </Button>
      </HStack>

      <Selection
        data-testid='selection'
        items={items}
        getItemId={c => c.id}
        value={selected}
        onChange={setSelected}
      >
        <VStack gap={12}>
          {items.length === 0 ? (
            <Text size='sm' color='secondary'>
              No items.
            </Text>
          ) : (
            items.map(c => (
              <SelectionItem key={c.id} itemId={c.id}>
                <ClusterCard cluster={c} />
              </SelectionItem>
            ))
          )}
        </VStack>

        <SelectionBulkBar>
          <Button color='brand' onClick={() => onDelete(selected)}>
            <Trash2 /> Delete
          </Button>
        </SelectionBulkBar>
      </Selection>
    </VStack>
  );
};
```

### Without bulk bar

No bar at all: the selection still works and the count is rendered from state, for a page
that acts on the selection somewhere else entirely.

```tsx
import { useState } from 'react';
import { Selection, SelectionItem } from '@wallarm-org/design-system/Selection';
import { VStack } from '@wallarm-org/design-system/Stack';
import { Text } from '@wallarm-org/design-system/Text';

export const WithoutBulkBar = () => {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <Selection
      data-testid='selection'
      items={clusters}
      getItemId={c => c.id}
      value={selected}
      onChange={setSelected}
    >
      <VStack gap={16}>
        <Text size='sm' color='secondary'>
          Selected: {selected.length}
        </Text>

        <VStack gap={12}>
          {clusters.map(c => (
            <SelectionItem key={c.id} itemId={c.id}>
              <ClusterCard cluster={c} />
            </SelectionItem>
          ))}
        </VStack>
      </VStack>
    </Selection>
  );
};
```

### Compound bulk bar

Composing the summary — count, select-all, clear — instead of taking the default. It has
to be a direct child of the bar; a wrapper component hides it and the default appears too.

```tsx
import { useState } from 'react';
import { Selection, SelectionItem } from '@wallarm-org/design-system/Selection';
import { VStack } from '@wallarm-org/design-system/Stack';

export const CompoundBulkBar = () => {
  const [items, setItems] = useState<Cluster[]>(clusters);
  const [selected, setSelected] = useState<string[]>([]);

  // Delete actually deletes here: a bulk action that only logged left nothing to
  // observe, and the row leaving is the honest demonstration of one.
  const deleteSelected = (ids: string[]) => {
    setItems(prev => prev.filter(c => !ids.includes(c.id)));
    setSelected([]);
  };

  return (
    <Selection
      data-testid='selection'
      items={items}
      getItemId={c => c.id}
      value={selected}
      onChange={setSelected}
    >
      <VStack gap={12}>
        {items.map(c => (
          <SelectionItem key={c.id} itemId={c.id}>
            <ClusterCard cluster={c} />
          </SelectionItem>
        ))}
      </VStack>

      <CompoundToolbar onDeleteSelected={deleteSelected} />
    </Selection>
  );
};
```

### Inside drawer

Inside a `Drawer`, `placement='absolute'` pins the bar to the bottom of the panel instead
of the viewport, and the actions step up to `large` to match the surface.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Drawer, DrawerBody, DrawerContent, DrawerHeader, DrawerResizeHandle, DrawerTitle, DrawerTrigger } from '@wallarm-org/design-system/Drawer';
import { Copy, Folder, Trash2 } from '@wallarm-org/design-system/icons';
import { Selection, SelectionAll, SelectionBulkBar, SelectionItem } from '@wallarm-org/design-system/Selection';
import { HStack, VStack } from '@wallarm-org/design-system/Stack';
import { Text } from '@wallarm-org/design-system/Text';

export const InsideDrawer = () => {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <Drawer width={720}>
      <DrawerTrigger asChild>
        <Button variant='outline' color='neutral'>
          Open drawer with selection
        </Button>
      </DrawerTrigger>
      <DrawerContent>
        <Selection
          items={clusters}
          getItemId={c => c.id}
          value={selected}
          onChange={setSelected}
          className='flex min-h-0 flex-1 flex-col'
        >
          <DrawerResizeHandle />
          <DrawerHeader>
            <DrawerTitle>Clusters</DrawerTitle>
          </DrawerHeader>
          <DrawerBody>
            <VStack gap={16}>
              <HStack gap={8} align='center'>
                <SelectionAll />
                <Text size='sm' color='secondary'>
                  Select all
                </Text>
              </HStack>

              <VStack gap={12}>
                {clusters.map(c => (
                  <SelectionItem key={c.id} itemId={c.id}>
                    <ClusterCard cluster={c} />
                  </SelectionItem>
                ))}
              </VStack>
            </VStack>
          </DrawerBody>

          <SelectionBulkBar placement='absolute'>
            <Button
              variant='secondary'
              color='neutral-alt'
              size='large'
              onClick={() => onDuplicate(selected)}
            >
              <Copy /> Duplicate
            </Button>
            <Button
              variant='secondary'
              color='neutral-alt'
              size='large'
              onClick={() => onMove(selected)}
            >
              <Folder /> Move
            </Button>
            <Button
              variant='secondary'
              color='destructive'
              size='large'
              onClick={() => onDelete(selected)}
            >
              <Trash2 /> Delete
            </Button>
          </SelectionBulkBar>
        </Selection>
      </DrawerContent>
    </Drawer>
  );
};
```

## Props

| Prop         | Type                      | Default | Description                                                        |
| ------------ | ------------------------- | ------- | ------------------------------------------------------------------ |
| items \*     | `T[]`                     |         | Source array — used for select-all and shift-click range ordering. |
| getItemId \* | `(item: T) => string`     |         | Stable id extractor for items.                                     |
| value \*     | `string[]`                |         | Controlled selection state — array of selected item ids.           |
| onChange \*  | `(ids: string[]) => void` |         | Called with the next ordered array of selected ids.                |
| data-testid  | `string \| undefined`     |         |                                                                    |

### SelectionAll

| Prop        | Type                  | Default | Description |
| ----------- | --------------------- | ------- | ----------- |
| data-testid | `string \| undefined` |         |             |
| aria-label  | `string \| undefined` |         |             |

### SelectionBulkBar

| Prop        | Type                                     | Default    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ----------- | ---------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| aria-label  | `string \| undefined`                    |            | Override the toolbar accessible name. Defaults to "Bulk actions".                                                                                                                                                                                                                                                                                                                                                                                                                        |
| data-testid | `string \| undefined`                    |            |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| placement   | `SelectionBulkBarPlacement \| undefined` | `floating` | How the bar is positioned. - `'floating'` (default): portaled to `document.body` and pinned to the bottom of the viewport with `position: fixed`. Use for page-level selection lists. - `'absolute'`: rendered in place with `position: absolute`. Anchors to the nearest positioned ancestor — by default the enclosing `Selection` root (which is `position: relative`), so the bar pins to the bottom of the selection area. Use this inside a Drawer or any other bounded container. |

### SelectionItem

| Prop      | Type                   | Default | Description                                                                              |
| --------- | ---------------------- | ------- | ---------------------------------------------------------------------------------------- |
| itemId \* | `string`               |         | Item id — must match an id present in the `<Selection>` `items` prop.                    |
| disabled  | `boolean \| undefined` | `false` | When true, the checkbox is disabled and the item is excluded from select-all and ranges. |

## Explorer

