---
url: https://docs.tailor.tech/app-shell/patterns/interaction-multi-select.md
description: Floating bottom action bar for bulk operations on selected list rows
---

# Multi Select

## When to Use

* ANY list page where rows can be acted on in bulk (archive, assign, export, approve, delete)
* Selection is initiated by clicking a leading-column checkbox on rows
* Selection state needs to persist across pagination and filter changes

## Layout

Floating action bar appears the moment selection count goes from 0 → 1, anchored to the bottom of the viewport, centered horizontally, with elevation. It disappears when selection returns to 0.

```
+---------------------------------------------------------+
| Layout.Header   title              [Filter] [Create]    |
+---------------------------------------------------------+
| Layout.Column                                           |
|  Table.Root                                             |
|   [x] | Col   | Col   | Col   | Col                    |
|   [x] | row   | row   | row   | row                    |
|   [ ] | row   | row   | row   | row                    |
|   [x] | row   | row   | row   | row                    |
|                                                         |
|        +--------------------------------------+         |
|        | 3 selected  [Archive] [Export] [⋯] [Clear] |   |
|        +--------------------------------------+         |
+---------------------------------------------------------+
```

## Page Implementation

```tsx
function InteractionMultiSelect() {
  const orders = mockOrders;
  const onArchive = (ids: string[]) => window.alert(`Archiving ${ids.length}`);
  const onExport = (ids: string[]) => window.alert(`Exporting ${ids.length}`);

  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());

  const toggleRow = (id: string) => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };

  const toggleAll = () => {
    if (selectedIds.size === orders.length) {
      setSelectedIds(new Set());
    } else {
      setSelectedIds(new Set(orders.map((o) => o.id)));
    }
  };

  const clearSelection = () => setSelectedIds(new Set());
  const selectedCount = selectedIds.size;

  return (
    <>
      <Table.Root>
        <Table.Header>
          <Table.Row>
            <Table.Head className="w-10">
              <input
                type="checkbox"
                checked={selectedIds.size === orders.length && orders.length > 0}
                onChange={toggleAll}
                aria-label="Select all on page"
              />
            </Table.Head>
            <Table.Head>Order #</Table.Head>
            <Table.Head>Status</Table.Head>
            <Table.Head>Total</Table.Head>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {orders.map((order) => (
            <Table.Row key={order.id}>
              <Table.Cell>
                <input
                  type="checkbox"
                  checked={selectedIds.has(order.id)}
                  onChange={() => toggleRow(order.id)}
                  aria-label={`Select ${order.number}`}
                />
              </Table.Cell>
              <Table.Cell>{order.number}</Table.Cell>
              <Table.Cell>{order.status}</Table.Cell>
              <Table.Cell>${order.total.toLocaleString()}</Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table.Root>

      {selectedCount > 0 && (
        <div
          role="toolbar"
          aria-label="Bulk actions"
          className="fixed bottom-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-2 rounded-md border bg-popover px-3 py-2 shadow-lg"
        >
          <span className="text-sm font-medium">{selectedCount} selected</span>
          <Button size="sm" onClick={() => onArchive([...selectedIds])}>
            Archive
          </Button>
          <Button size="sm" variant="outline" onClick={() => onExport([...selectedIds])}>
            Export
          </Button>
          <Menu.Root>
            <Menu.Trigger>
              <Button size="sm" variant="outline" aria-label="More actions">
                ⋯
              </Button>
            </Menu.Trigger>
            <Menu.Content>
              <Menu.Item>Assign owner</Menu.Item>
              <Menu.Item>Tag</Menu.Item>
              <Menu.Separator />
              <Menu.Item>Delete</Menu.Item>
            </Menu.Content>
          </Menu.Root>
          <Button size="sm" variant="ghost" onClick={clearSelection}>
            Clear
          </Button>
        </div>
      )}
    </>
  );
}
```

## Constraints

* Count label + Clear button are always present in the bar
* Max 3 inline action buttons — 4th onward collapse behind an overflow `Menu`
* Destructive bulk actions MUST open an `interaction/confirm` dialog
* Filter or sort change must NOT silently clear the selection
* Pagination MUST preserve selection across pages

## Anti-patterns

* Placing bulk-action buttons in the page header — bulk actions belong only in the floating bar
* Hiding the bar behind row hover or right-click — bar must be visible when selection > 0
* Omitting the count or the Clear affordance — both are mandatory
* Letting filter/sort changes silently drop selection
* Firing destructive bulk actions without an interaction/confirm step
* Per-row `Menu` actions as a substitute for bulk actions when selection > 0
