SelectionMode

Pick several things at once, on a screen or in a sheet.

Pick several things at once — messages to archive, people to share with, colours to apply, files to move.

It comes two ways round. On a screen it is a mode: the list is there to be read, and a long press turns it into one you can pick from. In a sheet it is a picker: SelectionMode.Sheet was opened in order to choose something, so it is choosing from the moment it appears, with the actions in the sheet's footer.

SelectionMode.Item wraps whatever you put in it rather than replacing it — a row, an avatar, a colour swatch, a slide thumbnail — so one component covers all of those without a prop for each.

For picking one thing, use Select or RadioGroup. For one row and the things you can do to it, use Swipe.

Alpha — the API is still moving. Expect it to change in a minor release.

Installation

SelectionMode ships with the library — no separate install.

import { SelectionMode, Item, Avatar, Text } from 'panelui-native';

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

npx panelui-cli@latest add selection-mode

Usage

const [open, setOpen] = useState(false);
const [selected, setSelected] = useState<string[]>([]);

<SelectionMode
  values={people.map((person) => person.id)}
  selected={selected}
  onSelectedChange={setSelected}
>
  <SelectionMode.Sheet open={open} onOpenChange={setOpen} title="Share with">
    {people.map((person) => (
      <SelectionMode.Item key={person.id} value={person.id}>
        <Item>
          <Item.Media>
            <Avatar fallback={person.initials} />
          </Item.Media>
          <Item.Content>
            <Item.Title>{person.name}</Item.Title>
            <Item.Description>{person.handle}</Item.Description>
          </Item.Content>
        </Item>
      </SelectionMode.Item>
    ))}

    <SelectionMode.Bar>
      <SelectionMode.Action icon={<SendIcon size={20} />} onPress={share}>
        Send
      </SelectionMode.Action>
    </SelectionMode.Bar>
  </SelectionMode.Sheet>
</SelectionMode>

Composition

<SelectionMode>
  {/* on a screen */}
  <SelectionMode.Header />
  <SelectionMode.Item>
    <SelectionMode.Indicator />
    {/* your item */}
  </SelectionMode.Item>
  <SelectionMode.Bar>
    <SelectionMode.Action />
  </SelectionMode.Bar>

  {/* …or in a sheet */}
  <SelectionMode.Sheet>
    <SelectionMode.Group>
      <SelectionMode.Item />
    </SelectionMode.Group>
    <SelectionMode.Bar />
  </SelectionMode.Sheet>
</SelectionMode>

Every part except the root reads the selection from context, so they all throw outside a SelectionMode rather than rendering something inert. useSelectionMode() is the same context, for a header or a count of your own.

Examples

Anything, not just rows

An item is a wrapper, so what it holds is yours. Group them in a card for rows, or pass columns for a grid of things recognised by sight — a swatch, a thumbnail, a slide. indicator="ring" draws the selection around the item instead of beside it, because a circle next to a colour is a second thing to look at.

<SelectionMode.Sheet open={open} onOpenChange={setOpen} title="Palette">
  <SelectionMode.Group columns={5} gap={14}>
    {palette.map((color) => (
      <SelectionMode.Item key={color.id} value={color.id} indicator="ring">
        <View
          style={{ backgroundColor: color.hex, aspectRatio: 1 }}
          className="w-full rounded-full"
        />
      </SelectionMode.Item>
    ))}
  </SelectionMode.Group>

  <SelectionMode.Bar>
    <SelectionMode.Action icon={<CheckIcon size={20} />} onPress={apply}>
      Apply
    </SelectionMode.Action>
  </SelectionMode.Bar>
</SelectionMode.Sheet>

On a screen, as a mode

A list that is there to be read, that you occasionally act on several rows of. A long press enters and picks the row that was pressed; the header says the mode is on and is the way out of it; the bar sits against the bottom edge. Leaving clears the selection, so it cannot come back the next time — control selected yourself to keep it.

<SelectionMode
  values={messages.map((message) => message.id)}
  selected={selected}
  onSelectedChange={setSelected}
>
  <SelectionMode.Header title="Choose" />

  <FlashList
    data={messages}
    keyExtractor={(message) => message.id}
    renderItem={({ item }) => (
      <SelectionMode.Item value={item.id} onPress={() => open(item)}>
        <Item>…</Item>
      </SelectionMode.Item>
    )}
    contentContainerClassName="pb-24"
  />

  <SelectionMode.Bar inset={insets.bottom}>
    <SelectionMode.Action icon={<TrashIcon size={20} />} destructive exitOnPress onPress={remove}>
      Delete
    </SelectionMode.Action>
  </SelectionMode.Bar>
</SelectionMode>

Select all, and a limit

Pass values and the header can pick everything at once. max caps it — a row that would go over the cap does not toggle on, and select all stops at the limit rather than refusing, because somebody who asked for all of them and can have twenty wants the twenty.

<SelectionMode values={slides.map((slide) => slide.id)} max={20}>
  <SelectionMode.Sheet open={open} onOpenChange={setOpen} title="Slides" />
</SelectionMode>

An item that cannot be picked

disabled keeps an item out of the selection entirely — it will not start a mode, and it cannot be picked once one is running. For the things in a list that are not among the things being chosen between: a section header, an advert, a "load more".

<SelectionMode.Item value={row.id} disabled={row.type === 'header'}>
  {/* … */}
</SelectionMode.Item>

A bar that floats instead

On a screen, placement="floating" lifts the bar off the edge into a rounded card — for a list that does not run to the bottom of the screen. Flush is the default because it does not take width away from the actions. In a sheet the bar is the footer and this does not apply.

<SelectionMode.Bar placement="floating" inset={insets.bottom}>
  <SelectionMode.Action icon={<TrashIcon size={20} />} destructive onPress={remove}>
    Delete
  </SelectionMode.Action>
</SelectionMode.Bar>

Variants

destructive

  • true
<SelectionMode destructive="true">…</SelectionMode>

placement

  • bar (default)
  • floating
<SelectionMode placement="bar">…</SelectionMode>
<SelectionMode placement="floating">…</SelectionMode>

API Reference

SelectionMode

PropTypeDefaultDescription
classNamestring
valuesstring[]Every value that can be picked, in list order. Only "select all" and the "n of m" in the header need it — picking rows one at a time works without it. Give it the same ids you give the list.
activebooleanControlled selection mode. Leave it out and a long press turns it on.
defaultActivebooleanfalseWhether selection mode starts on.
onActiveChange(active: boolean) => void
selectedstring[]Controlled selection.
defaultSelectedstring[]
onSelectedChange(selected: string[]) => void
maxnumberThe most that can be picked at once. A row that would go over it does not toggle on, and "select all" stops at the limit rather than refusing. Leave it out for no limit.
hapticsbooleanfalseA tick when a row is picked and when the mode is entered. Off by default — needs the optional expo-haptics, and is silent without it.

SelectionMode.Indicator

PropTypeDefaultDescription
classNamestring
valuestringWhich row this stands for. Defaults to the row it is inside.

SelectionMode.Item

PropTypeDefaultDescription
classNamestring
valuestringThis row's id. What ends up in selected.
onPress() => voidWhat the row does when it is pressed and the mode is off.
disabledbooleanfalseStop this row entering selection mode, and being picked once in it. For a header row, an advert, a "load more" — anything in the list that is not one of the things being chosen between.
alwaysShowIndicatorbooleanfalseDraw the circle without waiting for the mode.
indicator'leading' | 'ring' | 'none''leading'How being picked is drawn. leading puts the circle in front of the item, which is what a row wants. ring draws a ring around whatever you gave it instead — for a swatch, a thumbnail or a photo, where a circle beside it would be a second thing to look at and the item itself can carry the state. none draws nothing and leaves it to you; read useSelectionMode().isSelected.

SelectionMode.Group

PropTypeDefaultDescription
classNamestring
columnsnumberLay the items out in a grid this many across instead of stacking them. For things recognised by sight rather than read — swatches, thumbnails, slides. A grid of six colours is one glance; the same six as rows is a scroll.
gapnumber12Space between items in a grid, in points.
separatorsbooleantrueHairlines between stacked items. On by default; off in a grid.

SelectionMode.Header

PropTypeDefaultDescription
classNamestring
titlestring'Select'The word in front of the count.
hideSelectAllbooleanfalseHide the select-all control, for a list where picking everything is wrong.

SelectionMode.Bar

PropTypeDefaultDescription
classNamestring
insetnumber0Room under the actions, in points — your safe-area inset. A bar against the bottom edge sits over the home indicator on a phone that has one, and an action under a home indicator is an action that takes two tries. floating uses it as the gap on all four sides instead.
showWhenEmptybooleanfalseKeep the bar up with nothing picked. Off by default: every action on it needs something to act on, and a row of buttons that all refuse is worse than a row that is not there yet.

SelectionMode.Action

PropTypeDefaultDescription
classNamestring
iconReactNodeThe glyph above the label.
onPress(selected: string[]) => voidWhat it does. Handed the selection, so the common case needs no other wiring — and leaving the mode afterwards is up to you, because whether the list still makes sense depends on what you did to it.
exitOnPressbooleanfalseLeave selection mode after the action runs.
disabledbooleanfalse
labelClassNamestringExtra classes for the label.

SelectionMode.Sheet

PropTypeDefaultDescription
classNamestring
openbooleanControlled open state of the sheet.
defaultOpenboolean
onOpenChange(open: boolean) => void
titlestring'Select'The word in front of the count.
hideSelectAllbooleanfalseHide the select-all control.
size'auto' | 'half' | 'full''half'How tall the sheet opens. half by default, and deliberately not auto. A sheet that sizes to its content gives its scrolling body no height to fill, and a list inside a box of no height draws nothing — which looks like an empty sheet rather than like a missing style. A selection is a list; give it the room.

Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.

Notes

The sheet is half-height by default

Not auto. A sheet that sizes to its content gives its scrolling body no height to fill, and a list in a box of no height draws nothing — which reads as an empty sheet rather than as a missing style. Pass size="full" for a long list, or size="auto" only when the sheet really is a handful of rows.

Group the items

SelectionMode.Group is the rounded card with rules between its rows. One card of options reads as a set; the same rows loose on the sheet background read as a list that has not finished loading.

Which presentation

Use the sheet when choosing is the whole reason the surface exists — a share sheet, a palette, an attachment picker. Use the on-screen mode when the list has a life of its own and picking is something you occasionally do to it.

They are the same component and the same state; only the presentation differs. In a sheet there is no long press and no cancel button — the sheet dismisses itself — and the bar sits in the footer rather than over the list.

The circle is round for a reason

A square box is a form control somebody is filling in; a round one is a thing they are picking out of a set. Checkbox is the first of those and stays square.

On a screen, pad the bottom of your list

SelectionMode.Bar is drawn over the list rather than under it, because the list is as long as it is and a bar in the flow would be somewhere off the end of it. Nothing here can work out how tall your list is, so give it bottom padding — contentContainerClassName="pb-24" — or the last row sits under the bar forever.

Pass inset with your safe-area bottom inset, or the bar sits over the home indicator.

The bar is hidden until something is picked

Every action on it needs something to act on, and a row of buttons that all refuse is worse than a row that has not appeared yet. Pass showWhenEmpty if you would rather it were there the whole time.

Actions are handed the selection

onPress receives the picked values, so the common case needs no other wiring. Leaving afterwards is exitOnPress, and it is opt-in because whether the list still makes sense depends on what you just did to it — deleting means leaving, marking read might not.

Selection is a set of values

Ids, never indices or elements. A list that reorders, pages in more items or drops one underneath the reader would invalidate anything positional, and ids are also the shape the action at the end needs.

Give it a height

The root fills a height it is offered and takes its content's height when there is none. What it cannot do is fill a parent that has no height itself — put it in a container with flex-1, or give it a fixed height, the same as any list. In a sheet the sheet handles this.

On this page