Skip to content

PlTransfer

Two lists and the arrows between them: everything that could be chosen on one side, everything that has been on the other. Ticking is not choosing. The ticks say what the next press will move.

React
tsx
import { PlTransfer } from 'plass-ui';

<PlTransfer
  items={columns}
  value={value}
  onValueChange={setValue}
  sourceLabel="Available columns"
  targetLabel="In the report"
/>;
dart
import 'package:plass_ui/plass_ui.dart';

PlTransfer(
  items: columns,
  value: value,
  onValueChanged: (List<String> next) => setState(() => value = next),
  sourceLabel: 'Available columns',
  targetLabel: 'In the report',
);

Props

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost''glass'What the two panels are made of. The same shell a field wears: a list holds a value rather than being pressed
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'The checkboxes, the arrows, the type scale and the padding, moving together
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''default'Padding only — never the height, never the type scale
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 means no shadow at all
items * readonly PlTransferItem[]Everything that can be on either side, in the order both lists show it
valuereadonly string[]What is on the trailing side. Use with onValueChange for a controlled pair
defaultValuereadonly string[]What starts there, for an uncontrolled one
onValueChange(value: string[]) => voidCalled when something moves across or back
sourceLabelReactNode'Available'The heading over the leading list
targetLabelReactNode'Selected'And over the trailing one
searchablebooleanfalsePuts a filter above each list
searchLabelstring'Search'What that filter says while it is empty
emptyLabelstring'Nothing here'What a list with nothing in it says
selectAllLabelstring'Select all'What the tick in a list's heading is announced as
toTargetLabelstring'Move to selected'What the outward arrow is announced as
toSourceLabelstring'Move to available'What the returning arrow is announced as
heightnumber | string220How tall each list is. A number of pixels or any CSS length
disabledbooleanfalseNothing can be ticked or moved
PropTypeDefaultDescription
variantsharedPlassVariantPlassVariant.glassWhat the two panels are made of. The same shell a field wears: a list holds a value rather than being pressed
sizesharedPlassSizePlassSize.mdThe checkboxes, the arrows, the type scale and the padding, moving together
colorsharedPlassColorPlassColor.primarySemantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensityPlassDensity.standardPadding only — never the height, never the type scale
items * List<PlTransferItem>Everything that can be on either side, in the order both lists show it
valueList<String>?What is on the trailing side. Use with onValueChange for a controlled pair
defaultValueList<String><String>[]What starts there, for an uncontrolled one
onValueChangedValueChanged<List<String>>?Called when something moves across or back
sourceLabelString'Available'The heading over the leading list
targetLabelString'Selected'And over the trailing one
searchableboolfalsePuts a filter above each list
searchLabelString'Search'What that filter says while it is empty
emptyLabelString'Nothing here'What a list with nothing in it says
selectAllLabelString'Select all'What the tick in a list's heading is announced as
toTargetLabelString'Move to selected'What the outward arrow is announced as
toSourceLabelString'Move to available'What the returning arrow is announced as
heightdouble220How tall each list is. A number of pixels or any CSS length
disabledboolfalseNothing can be ticked or moved

Every native <div> attribute passes straight through. color is excluded because it is a Plass prop here, defaultValue because the pair spells it as a list of values, and onChange because the pair reports through onValueChange.

PlTransferItem

PropTypeDefaultDescription
value * stringWhat identifies it, and what value is a list of
label * ReactNodeWhat the row says
disabledbooleanfalseIn the list but not movable
PropTypeDefaultDescription
value * StringWhat identifies it, and what value is a list of
label * StringWhat the row says. A String rather than a widget, so there is no row the filter cannot read
disabledboolfalseIn the list but not movable

What the shared axes (variant size color density elevation) mean across the library is in prop conventions.

When to use it

For a choice that is long. A PlCombobox with forty chips in its field stops being readable, and a list of forty checkboxes gives no answer to "what did I actually pick".

Below about a dozen options, one of those two is the smaller component. This one costs a reader two lists and a pair of arrows; it is worth it exactly when the answer is itself worth reading back.

Ticking is not choosing

value is which side a row is on. The ticks are which rows the next press will move, and they are a separate piece of state on purpose: keeping them apart is what makes a press a deliberate act rather than a side effect of reading down a list.

Three things follow from it:

  • The order of items is the order both lists show, so a row does not move when it is sent across and back.
  • Moving drops the ticks on what moved and keeps the rest. A row that has arrived on the other side is not still waiting to be sent there.
  • A row the filter was hiding was never part of that press.

Examples

searchable

Puts a filter above each list, and each one narrows only its own side.

The fold is case- and accent-insensitive, cafe finds Café, and it is the library's one answer to "this matches what I typed", shared by every filter in it. A reader who has learned what one search box in a product does has learned the right thing about the next one.

A label that is a node rather than a string has no text to match and stays. The alternative is a row that disappears from a filter it could never satisfy.

React

variant

The two panels wear the field shell rather than the sheet one, because a list that holds a value is a field-shaped thing: solid is the well, glass the hairline pane, ghost no surface until the pointer arrives. The arrows follow. They are glass beside a panel that has an edge, and ghost beside one that has none.

Neither panel is dyed. What they hold is somebody's data, and the family reaches the ticks, the arrows and the focus rings.

React

A row that cannot move

disabled on an item leaves it in the list and takes it out of every press, including the heading's select-all, which is why that tick reports three of four rather than four of four. An option that vanished when it could not be chosen is an option the reader will look for.

disabled on the pair stops everything at once.

React

Controlled

Pass value with onValueChange. The value is the list of values on the trailing side, in items order, not the objects: a transfer is a form control, and what it holds is what the form submits. Keep the identifiers here and look the objects up on the other side.

Differences from the React build

ReactFlutterWhy
label of ReactNode, a non-string one kept by the filterlabel of StringThe filter reads the label, and a row it cannot read is a row that disappears from a search it could never satisfy. Making it text is what keeps every row searchable by construction.
the fold strips case and combining markscase onlyDart's core has no String.normalize, and this package has no dependencies. Pulling one in so a search box folds accents would put it in every consumer's binary for the sake of one comparison.
height as a number or a CSS lengthheight as a doubleThere is no second unit to name.
onValueChangeonValueChangedFlutter's name.
className, style, native attributesThere is no class list and no style attribute to pass through.

Accessibility

  • Every row is a real PlCheckbox with the row's label as its name, so a screen reader reads the list as the list of checkboxes it is.
  • The tick in each heading is a checkbox too, named by selectAllLabel, and it reports indeterminate when only some of its list is ticked.
  • The two arrows are PlIconButtons with real names, and they are disabled until a press would actually move something. The state a reader can see, made available to one who cannot.
  • Each list has its own count (ticked/total) beside its heading, which is the answer to "how much did I just select" without counting rows.
  • The lists scroll on their own and hold their scroll position, so moving a row does not throw a reader back to the top.

Released under the MIT License