SelectionMode

Pick several things at once — a long press enters the mode — then act on them from a bar.

Open Source

Installation

SelectionMode ships with the library — install it once, then import:

import { SelectionMode } from "@delacour/react-native-ui/selection-mode";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add selection-mode

Usage

app/index.tsx
import { SelectionMode } from "@/components/ui/selection-mode";
import { Item } from "@/components/ui/item";
import { IconArchive, IconTrashCan } from "@/lib/icons/central";
import { useState } from "react";
import { FlatList, View } from "react-native";

export default function Inbox() {
  const [messages, setMessages] = useState(MESSAGES);
  const remove = (ids: string[]) => setMessages((list) => list.filter((m) => !ids.includes(m.id)));

  return (
    <View className="flex-1">
      <SelectionMode values={messages.map((m) => m.id)}>
        <SelectionMode.Header title="Inbox" />
        <FlatList
          contentContainerClassName="pb-24"
          data={messages}
          renderItem={({ item }) => (
            <SelectionMode.Item value={item.id} onPress={() => open(item)}>
              <Item>
                <Item.Content>
                  <Item.Title>{item.from}</Item.Title>
                  <Item.Description>{item.subject}</Item.Description>
                </Item.Content>
              </Item>
            </SelectionMode.Item>
          )}
        />
        <SelectionMode.Bar>
          <SelectionMode.Action icon={IconArchive} isExitOnPress onPress={remove}>
            Archive
          </SelectionMode.Action>
          <SelectionMode.Action icon={IconTrashCan} isDestructive isExitOnPress onPress={remove}>
            Delete
          </SelectionMode.Action>
        </SelectionMode.Bar>
      </SelectionMode>
    </View>
  );
}

Anatomy

<SelectionMode values={ids}>
  <SelectionMode.Header />
  <SelectionMode.Group>
    <SelectionMode.Item value="a">…</SelectionMode.Item>
  </SelectionMode.Group>
  <SelectionMode.Bar>
    <SelectionMode.Action onPress={act}>Act</SelectionMode.Action>
  </SelectionMode.Bar>
</SelectionMode>

SelectionMode.Item wraps what it holds — a row, an avatar, a swatch — rather than replacing it. SelectionMode.Group is optional: any list works, FlatList included.

It is a mode

With the mode off the list is for reading, and each item runs its own onPress. A long press turns the mode on with the pressed item picked. From then on a press toggles, and the item's own onPress does not run. The header says the mode is on, counts the picks and offers the way out; the bar acts on what is picked.

Inbox mode

Closing the header, or calling exit(), clears an uncontrolled selection. A controlled selected is left alone — you may want a draft that survives leaving the mode.

For an always-on picker, hold the mode on with isActive and show the marks from the start with isIndicatorAlwaysShown:

Share picker
<SelectionMode className="flex-none" isActive defaultSelected={["rawiri"]}>
  <SelectionMode.Group label="Share with">
    {people.map((person) => (
      <SelectionMode.Item isIndicatorAlwaysShown key={person.id} value={person.id}>
        <Item>…</Item>
      </SelectionMode.Item>
    ))}
  </SelectionMode.Group>
  <ShareButton />
</SelectionMode>

useSelectionMode() reads the selection from anywhere inside the root:

function ShareButton() {
  const { count } = useSelectionMode();
  return <Button isDisabled={count === 0}>{`Share with ${count}`}</Button>;
}

Selection is ids

The selection is a string[] of item values, in the order they were picked — never indices. An index changes meaning the moment a list is sorted, filtered or paged; an id does not, so a selection survives a refetch. Pass values — every pickable id, in list order — and the header counts "n of m" and offers select-all; without it the header reads "n selected".

Give it a height, pad the list

The root fills the height it is offered (flex-1), and the bar is drawn over the bottom of the list rather than shrinking it. So:

  • Give the root a height. Put it in a screen or a sized parent. A flex-1 root in a parent with no height collapses to nothing. A root that should take its content's height — a picker with no bar — takes className="flex-none".
  • Pad the bottom of your list (contentContainerClassName="pb-24") so the last row scrolls clear of the bar.

A cap

max caps both a press and select-all. At the cap an unpicked item does not go on, but a picked one still comes off, and select-all reads "Deselect all" because there is nothing left for it to add.

A cap of three
<SelectionMode defaultActive max={3} values={toppings.map((t) => t.id)}>
  <SelectionMode.Header title="Toppings" />
  …
</SelectionMode>

Disabled items

isDisabled items never toggle and never start the mode — but their own onPress still runs, even while the mode is on. That is what a "Load more" row or a section header needs. Leave them out of values too, so select-all and the count skip them.

<SelectionMode.Item indicator="none" isDisabled onPress={loadMore} value="load-more">
  <Item>…</Item>
</SelectionMode.Item>

Indicators

indicatorDraws
leading (default)A round mark that slides in from the leading edge and pushes the content 34pt. A picked row takes a faint primary fill
ringA 2pt primary ring a 2pt gap outside the item, with a check badge at the top end. ringClassName reshapes it — rounded-full round a swatch
noneNothing. Read isSelected from useSelectionMode() and draw your own

The indicator is round on purpose. A square box is a form control being filled in; a round one is a thing picked out of a set. Checkbox stays square.

Swatch grid

Layouts

SelectionMode.Group lays its items out three ways:

<SelectionMode.Group>…</SelectionMode.Group>
<SelectionMode.Group columns={5}>…</SelectionMode.Group>
<SelectionMode.Group isHorizontal itemWidth={40}>…</SelectionMode.Group>

A stack is a card with hairlines between rows. A grid splits its measured width evenly between columns. A strip scrolls sideways with room around each item for a ring. isHorizontal wins over columns.

The bar

edge (default) spans the bottom of the root and pads itself with the safe-area inset. floating lifts the actions into an inset rounded card. The bar is hidden while nothing is picked unless isShownWhenEmpty, and never shows with the mode off.

Floating bar

Each action's onPress is handed the selection as it stands at the press, so a handler never closes over a stale list. isExitOnPress leaves the mode afterwards — right for a delete, whose picks no longer exist.

In a sheet

There is no sheet part, so the optional sheet engine is not forced on every consumer. Compose one: wrap the BottomSheet in SelectionMode — context reaches through the sheet's portal — and put the bar in the sheet's sticky footer.

In a sheet
<SelectionMode className="flex-none" isActive values={channelIds}>
  <BottomSheet>
    <BottomSheet.Trigger asChild>
      <Button>Post to channels</Button>
    </BottomSheet.Trigger>
    <BottomSheet.Portal>
      <BottomSheet.Overlay />
      <BottomSheet.Container>
        <BottomSheet.ScrollView>
          <SelectionMode.Group>…</SelectionMode.Group>
        </BottomSheet.ScrollView>
        <BottomSheet.Footer sticky>
          <SelectionMode.Bar className="relative border-t-0 bg-transparent" isSafeAreaAware={false} isShownWhenEmpty>
            <SelectionMode.Action icon={IconPaperPlane} onPress={post}>Post</SelectionMode.Action>
          </SelectionMode.Bar>
        </BottomSheet.Footer>
      </BottomSheet.Container>
    </BottomSheet.Portal>
  </BottomSheet>
</SelectionMode>

Motion

The mark pops on a spring, the header fades in from 8pt above, and the bar slides up. Under reduced motion every one of them becomes a 150ms fade — never nothing, because the change is the information that the mode moved.

Accessibility

While the mode is on, an item is a checkbox with accessibilityState={{ checked, disabled }} and the hint "Double-tap to select". While it is off, the item keeps the semantics of what it wraps and adds a "Start selecting" action, so the mode can be entered without the long press. The header announces the count as it changes. A group's label is its accessibility label, and a grid or strip is a list.

API Reference

SelectionMode

The root. Owns the mode and the selection. Extends ViewProps.

Prop

Type

SelectionMode.Item

Prop

Type

SelectionMode.Header

Renders only while the mode is on. Extends ViewProps.

Prop

Type

SelectionMode.Bar

Absolute over the bottom of the root. Extends ViewProps.

Prop

Type

SelectionMode.Action

Prop

Type

SelectionMode.Group

Extends ViewProps.

Prop

Type

SelectionMode.Indicator

The round mark on its own, for an item drawn with indicator="none". Reads the enclosing item, or takes a value.

Prop

Type

useSelectionMode

Returns { isActive, selected, isSelected, toggle, selectAll, clear, enter, exit, count, total, max, isAllSelected }. Every part but the root, and this hook, throws outside a <SelectionMode>.

On this page