1 slot open this monthBy the makers of shadcnregistryWe built shadcnregistry. Now we build your product.Book a callCheck availability
1 slot open this monthAI features?We build AI features into your product.Book a callCheck availability
1 slot open this month🚨 Slop police🚨 Slop police: we clean up vibe-coded apps.Report itReport your app
shadcnregistry
shadcnregistry
RegistryHealthInspect
Terms of ServicePrivacy Policy
  1. Registries
  2. pulld
  3. Transfer List

Transfer List

ui

Two lists side by side and the controls that move rows between them — the dual-listbox or "pick list" pattern: what is available on the left, what has been chosen on the right, and four named buttons in the middle. Reach for it wherever the question is which of these and the answer is long enough that seeing what is in and what is out at the same time beats a dropdown: permissions, scopes and API-token access on an admin screen, assigning roles or groups to a user, picking the members of a team, project, channel or on-call rotation, choosing which columns a table shows or which fields an export, report or integration syncs, building the recipient list of an email, invite or notification, selecting which metrics a dashboard plots, which locations, stores or warehouses a rule covers, which tags, categories or facets hang off a post or product, which features a plan includes, which modules a course or curriculum contains, moving players between a squad and a bench, and the "applies to" of any policy, coupon or automation. Common asks it answers: "transfer list", "transfer list react", "dual listbox", "dual list box react", "shadcn transfer list", "shadcn dual listbox", "pick list component", "picklist react", "two list multi select", "move items between two lists react", "move selected items left and right", "arrow buttons between two lists", "available and selected lists react", "side by side multi select", "MUI TransferList equivalent", "antd Transfer component alternative", "PrimeReact PickList alternative", "react-dual-listbox alternative", "react-widgets Multiselect two columns", "shadcn assign permissions ui", "permission assignment component", "role assignment two lists", "column picker react", "choose which columns to show component", "table column chooser react", "field picker component", "recipient picker react", "add and remove between lists", "select all and move all button", "move all visible rows", "filter both lists before moving", "search inside transfer list", "disabled items in transfer list", "locked rows cannot be moved", "multi select listbox accessible react", "aria-multiselectable listbox example", "aria-activedescendant react listbox", "role listbox multiple selection", "accessible listbox keyboard react", "space to select enter to move listbox", "focus lost after moving an item between lists", "disabled button steals focus react", "focus jumps to body when button becomes disabled", "where should focus go after moving an item", "aria-activedescendant points at removed option", "screen reader silent after moving items", "announce move between lists aria-live", "order changes when I move an item back", "item appended to end instead of its original position", "list reorders itself after undo", "move button says right arrow", "accessible name for arrow buttons", "二つのリストで項目を行き来させる", "振り分け UI react", "権限の割り当て 画面", "ロールの割り当て コンポーネント", "表示する列を選ぶ UI", "左右のリストで移動", "選択済みと未選択を並べて見せる", "dual listbox 実装", "移動ボタンの読み上げ名", "戻したら並び順が変わる", "リストボックス 複数選択 アクセシビリティ", "全部移動 ボタン", "フィルタしてから移動". Official shadcn/ui has nothing of this kind, and the measurement is more surprising than the usual zero. Taken today over its whole registry — 63 items in the catalogue, 62 fetchable (questionnaire is indexed and 404s on both style tracks), 223,287 bytes of files[].content concatenated — TransferList, transfer-list, dualListbox, selectedItems and moveRight are all zero hits, which is expected. So are listbox, role="option", aria-activedescendant, aria-multiselectable and the word multiple: official ships no listbox markup at all, Radix owning it inside select (6,343 bytes) and combobox (9,169 bytes), so the whole ARIA pattern this component is built on appears nowhere in the registry to copy from. The single aria-selected in all 223KB is a Tailwind variant in calendar styling react-day-picker's output (aria-selected:text-muted-foreground) — a styling hook, not a list. The things official has that hold more than one answer are a different tool: checkbox is 1,204 bytes of one control per option, toggle-group 2,309 bytes of always-visible buttons, and both stop working somewhere around a dozen options. Five things go wrong in the version everybody writes first, and four of them are invisible to a mouse. First, where the cursor goes after a move — two different failures wearing the same face. Empty a side with the button you are holding and that button can no longer act: given the disabled attribute it stops being focusable while it holds focus, so the browser drops focus to <body>, the next Tab restarts at the top of the document, and a screen reader says nothing. Every unavailable control here is aria-disabled instead, so it keeps its tab stop, keeps the focus it had, and announces itself as unavailable. Move from inside a list, though — Enter on a selection, a double-click — and focus never left the listbox; what is lost is aria-activedescendant, still pointing at an option that is now on the other side, so the ring is drawn and there is nothing to read. nextActiveAfterMove is one subtraction rather than two rules: the new position is where the cursor was minus however many removed rows were above it, clamped to the end. That answers both the row under the cursor having moved away (it lands on the row that took the slot, so emptying a list is presses in one place) and the row under the cursor having stayed (it follows that row), which is the case a rule written only for the first gets wrong — move a selection from the top of a list while standing in the middle of it and "go to the first vacated slot" yanks the cursor to the top for no visible reason. Second, the two sides are listboxes and not columns of checkboxes: aria-multiselectable with aria-selected on every option is a list of which several are chosen, where checkboxes are independent controls, one tab stop each, announced one at a time with no sense of how many there are. Third, the order rows come back in. Push onto the destination and a row moved right then left lands at the bottom of the list it came from, so changing your mind rearranges the thing you were reading; do it twice and the order is one nobody chose. Neither side stores an order here — both are items filtered, so the canonical order is the one you sorted items into and a move is reversible in the only sense that matters. The emitted array is in that order too, so the same choice is always the same array and nothing downstream diffing it sees a change that is not one. Fourth, the names of the move buttons: four arrows announce as four arrows, and two of them are "move the three I picked" and "move all four hundred". Each is named for its effect and its true count — "Move 3 selected to Granted", "Move all 12 to Granted" — where the count is what will actually move, locked rows excluded, so the name never over-promises. Fifth, both empty states: an empty side keeps its tab stop, because the side nobody can reach by keyboard is exactly the one about to be moved into, and it says whether it is empty or merely filtered to nothing, which are different problems. A move is also announced through a polite live region, because it changes two lists and no field's value — without it the only evidence a press did anything is that two numbers changed. What you get: TransferList, taking items (every row, in the order both sides are shown in) and the chosen values controlled with value + onChange (handed the next array and a { to, values } saying what went where) or uncontrolled with defaultValue; per-item disabled for rows that are shown, readable, focusable and never moved; a filter box on each side, on by default and off with searchable={false}, which narrows its side for every purpose so nothing you cannot see is ever moved, while a selection you filtered out of sight is kept rather than destroyed; availableLabel and chosenLabel, the one pair of words every button, count and announcement is built from ("All permissions" and "Granted" rewrites the lot), with labels to override any single one or translate it; label, description, name and form for an ordinary form POST as one hidden input per value — disabled mirrored onto them, because a disabled control is left out of the form data and a hidden input that was never told so is not; and per-part className for the group, the listboxes, an option and the buttons. The keyboard is complete on both sides: one tab stop each, arrows to move the cursor, Space to pick, Shift+Arrow to extend by adding, Home and End, Ctrl/Cmd+A for everything movable on screen, Enter to move what is picked (or the row under the cursor when nothing is), and a double-click for one row. transferListSides, transferValues, movableValues and nextActiveAfterMove are exported as pure functions, so the decisions that are actually hard — which side a row is on, what the next array is, what will really move, and where the cursor lands — are assertable and usable headless; a chosen value that no item claims is carried through a move untouched, so a paginated or lazily loaded items does not silently drop choices made on another page. One file, five lucide icons, no Radix and no state library. Distinct from its neighbours by what it shows rather than what it returns: multi-select picks from a list inside one field and shows the chosen as badges, nested-checkbox selects from a hierarchy, tag-input takes free text that did not exist before, and tree-view is hierarchy you navigate rather than select — none of them puts both sets on screen at once, which is the thing this is for. sortable-list is the one to pair it with rather than replace it with: this answers which, not in what order, and if the order of the chosen set is itself the decision then feed one into the other. bulk-action-bar acts on rows already selected, and filter-chips shows which conditions are in force.

pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/transfer-list.json"
Premium blocks for shadcn/uiShadcnblocks.com · AffiliateExplore
  • lucide-react
    components/ui/transfer-list.tsx