Source Code

<div class="tl" id="tl">
  <div class="tl-side">
    <div class="tl-head"><span>Available</span><span class="tl-count" data-for="available">0</span></div>
    <input class="tl-search" type="search" placeholder="Filter…" data-list="available" aria-label="Filter available">
    <ul class="tl-box" data-list="available" role="listbox" aria-multiselectable="true"></ul>
  </div>

  <div class="tl-controls">
    <button type="button" class="tl-btn" data-act="add-all" title="Move all right">&raquo;</button>
    <button type="button" class="tl-btn" data-act="add" title="Move selected right">&rsaquo;</button>
    <button type="button" class="tl-btn" data-act="remove" title="Move selected left">&lsaquo;</button>
    <button type="button" class="tl-btn" data-act="remove-all" title="Move all left">&laquo;</button>
  </div>

  <div class="tl-side">
    <div class="tl-head"><span>Selected</span><span class="tl-count" data-for="selected">0</span></div>
    <input class="tl-search" type="search" placeholder="Filter…" data-list="selected" aria-label="Filter selected">
    <ul class="tl-box" data-list="selected" role="listbox" aria-multiselectable="true"></ul>
  </div>
</div>

Transfer List — Dual List Box to Move Items Between Lists

Transfer List · Forms · Plain HTML, CSS & JS · Live preview

What's included

Features

Dual list box
Move items between Available and Selected pools.
Multi-select
Click to toggle multiple items, then move them together.
Move-all controls
Transfer the entire list in one click, in either direction.
Double-click move
Double-clicking an item sends it across instantly.
Per-list filtering
Each side has its own case-insensitive search box.
Smart button state
Controls disable when there is nothing to move.
Listbox ARIA
role=listbox/option with aria-selected for the multi-select model.
No library
Pure HTML/CSS/JS — no UI-kit transfer component.

About this UI Snippet

Transfer List — Dual List Box with Multi-Select, Filtering and Move-All

Screenshot of the Transfer List snippet rendered live

A transfer list — also called a dual list box — lets users move items between two lists: an "available" pool and a "selected" set. It's the classic control for assigning permissions, picking columns, building playlists, or choosing recipients. This snippet builds a complete one with multi-select, per-list filtering, move-selected and move-all buttons, and double-click-to-move, in plain HTML, CSS, and vanilla JavaScript.

A single source of truth

Both lists are rendered from one state object holding two arrays (available and selected) plus a selection set per side. Every action mutates those arrays and re-renders, so the UI can never drift from the data — there's no manual DOM shuffling that risks duplicate or orphaned items. Moving an item splices it out of one array and pushes it into the other, then re-sorts the destination so the lists stay alphabetised.

Multi-select and the four controls

Click an item to toggle its selection (highlighted), then use &rsaquo; to move the selected items right or &lsaquo; to move them left. &raquo; and &laquo; move everything at once. The buttons disable themselves intelligently: "move selected" is greyed out when nothing is selected on that side, and "move all" is greyed out when a side is empty — so the controls always reflect what's actually possible. Double-clicking an item moves it immediately, the shortcut power users expect.

Per-list filtering

Each side has its own search box that filters that list as you type, matching case-insensitively. Filtering is purely a view concern — it never changes the underlying arrays — so moving a filtered subset still works correctly and clearing the filter restores the full list. This matters when the available pool has dozens or hundreds of items.

Accessible structure

The lists use role="listbox" with aria-multiselectable, and each item is a role="option" carrying aria-selected, so assistive tech understands the multi-select model. The counts in each header announce how many items each side holds, and the move buttons have descriptive title attributes. Because everything is real, focusable markup, it's straightforward to extend with full keyboard navigation.

Responsive

On narrow screens the two lists stack vertically and the control column rotates to sit between them, so the same interaction works on a phone. The whole component is a couple of hundred lines with no dependencies, making it a clean, drop-in reference for the transfer-list pattern that UI kits charge a component for.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than tracing the move() and render() calls by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why moving items reads selectedKeys from the sel object and calls state[to].sort() instead of just appending to the destination array. It's also a good target for optimization questions — ask whether re-rendering both full lists on every keystroke in the search boxes could be replaced with something that only touches the changed rows once the item counts grow into the thousands. For extending it, have it add keyboard support so arrow keys and space toggle selection without a mouse, drag-and-drop reordering within a list, or grouped/nested items with a select-all-in-group checkbox. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "transfer list" (dual list box) in plain HTML, CSS, and vanilla JavaScript with no libraries.

Requirements:
- Two side-by-side lists, Available and Selected, each backed by its own array in a single state object, plus a per-list object tracking which labels are currently selected.
- Clicking an item toggles it into or out of that list's selection set and re-renders just that list, highlighting selected items visually and via aria-selected.
- Double-clicking an item moves it immediately to the other list.
- Four buttons: move-selected-right, move-selected-left, move-all-right, move-all-left. Moving an item must splice it out of the source array, push it into the destination array, delete it from the source's selection set, then re-sort the destination array alphabetically before re-rendering both lists.
- Each button must disable itself automatically based on current state: the move-selected buttons disable when that side has nothing selected, and the move-all buttons disable when that side's array is empty.
- Each list has its own case-insensitive text filter input that hides non-matching items by re-rendering, but must never mutate the underlying arrays — moving "all" while a filter is active must still move every item in the real array, not just the visible ones.
- Use role="listbox" and aria-multiselectable on each list container and role="option" with aria-selected on each item for accessibility.
- On viewports under 560px, stack the two lists vertically and rotate the middle button column 90 degrees so it reads correctly between them.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSTwo lists render — Available is populated, Selected starts empty.
  2. 2
    Set your itemsReplace the DATA array with your own labels (or objects, with a small tweak).
  3. 3
    Select and moveClick items to select, then use the ›/‹ buttons, or double-click to move one.
  4. 4
    Move everythingThe »/« buttons transfer all items at once between the lists.
  5. 5
    Filter long listsType in either search box to narrow that side as you go.
  6. 6
    Read the resultstate.selected holds the chosen items — submit or sync it as needed.

Real-world uses

Common Use Cases

Assigning permissions or roles
Pick capabilities for a user in a settings panel.
Choosing table columns
Move columns in/out of view alongside a column toggle.
Picking recipients
Build a send list from a directory of people or teams.
Configuring integrations
Enable a subset of services from an integration cards catalog.
Tag and category assignment
Assign many tags at once instead of one chip at a time.
Learning list-state UI
A reference for single-source-of-truth rendering and filtering.

Got questions?

Frequently Asked Questions

It is a control with two side-by-side lists — an available pool and a selected set — and buttons to move items between them. It is ideal when users assign a subset from a larger collection, such as permissions, columns, or recipients, because it shows both what is chosen and what remains in one view, unlike a multi-select dropdown.

Read state.selected — it is the array of labels currently in the Selected list, kept sorted. Mirror it into a hidden input, post it with fetch, or bind it to your framework state at submit time. Because every move updates that array, it is always an accurate reflection of the UI.

No. The search boxes only change what is displayed; they never alter the underlying arrays. So if you filter the Available list and click "move all," only the visible behaviour you would expect is affected — the move operations work on the real state, and clearing a filter restores the full list intact.

Yes. Click several items to select them (they highlight), then use the move-selected button to transfer them together. The move-all buttons send the entire list across in one click, and double-clicking a single item is a quick shortcut to move just that one.

Hold the two arrays and the selection sets in component state and render both lists from them — useState (React), reactive refs (Vue), or component fields (Angular). The move and filter functions become methods that update state instead of calling render(); the framework re-renders for you. Keep the role=listbox/option markup for accessibility, and in Tailwind swap the classes for utilities.