How to Build a Material-UI Transfer List from Checkboxes and Grids

Material-UI docs contain a useful pre-built component called a “Transfer List”. You’ve likely used transfer lists before: swap selections from one side to the other and back again.

In this article I’ll take Material-UI’s enhanced transfer list and, well, enhance it even more.

Material-UI Transfer List

I’ll style with elevation and box shadow, plus add in the sort and total swap functionality. I’ll also add a collapse action in the card header. The rest of the functionality is straight from the example in the docs.

As importantly, I’ll discuss and explain the underlying transfer list component from the docs.

For an introduction to the Material-UI Grid component, read here.


This is a trimmed down version of the JSX. Many styling props have been removed, but the structure is there:

<Grid item>
<Paper>{CardSelector("Choices", left, "left")}</Paper>
<Grid item>
<Grid container direction="column" alignItems="center">
<Grid item>
<Paper>{CardSelector("Chosen", right, "right")}</Paper>

The Transfer List is not really a unique component at all, but a combination of components. It’s structure is provided primarily by a <Grid> that contains three columns. The outer columns are the <CardSelector> components, the inner column is populated with buttons.

This structure was in the original Code Sandbox from the Material-UI docs. I added the <SwapHorizIcon> button.

MUI CardSelector JSX

Another trimmed down JSX:

<Card style={{ display: "flex", flexDirection: "column" }}>
<Checkbox />
subheader={`${numberOfChecked(items)}/${items.length} selected`}
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
<Collapse in={expanded} className={classes.collapse}>
<List className={classes.list} dense component="div" role="list">
{ number) => {
return (
<Checkbox />
<ListItemText />
<ListItem />
onClick={(e) => handleSortClick(e, items, side)}

This is the JSX for CardSelector (what the original sandbox called ‘customList’). It is a <Card> which wraps <CardHeader>, <List>, and <Button> components.

I chose to add the sort button to the bottom of the CardSelector as a kind of footer. Another choice would have been to add it as the action prop of the <CardHeader>, and use an icon instead of text I probably would have used the <SwapVert> icon.

I added the <Collapse> component which wraps the list and sort button and paired it with action in the <CardHeader>. When the action is triggered, the <Collapse> is notified via the expanded state var. An important item to notice is that each CardSelector gets its own expanded state var to track state with:

const CardSelector = (
title: React.ReactNode,
items: number[],
side: string
) => {
const [expanded, setExpanded] = React.useState(true);
//...more JSX

The most interesting thing about the original CardSelector code is that the avatar prop of <CardHeader> is actually the checkbox for selecting all. <CardHeader> was a clever choice for organizing the select all functionality and text, and looping through a list of checkboxes is a natural choice for displaying the content.

Here’s how to style and use the MUI ListItemText component.

Sorting, Swapping, and Re-Rendering the Transfer List

Sorting and swapping update the left and right state variables in order to update the UI. For example, here’s the sort function:

const handleSortClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
items: number[],
side: string
) => {
const copyItems = [...items].sort((a, b) => a - b);
if (side === "right") {
} else {

Notice how I had to use the spread operator to create a copy of the items array. This copy was then passed to the setters for the state variables. If the state variables were internally sorted, React would not be aware of the sort and would not re-render the DOM. (Another way around this would be to use an observable.array with MobX).

Here’s the swap function: 

const handleSwap = () => {

A useful thing about this simple function is that selected state is maintained in the swap.

More interestingly, let’s discuss the not, intersection, and union functions in the original sandbox.

The not function:

function not(a: number[], b: number[]) {
return a.filter((value) => b.indexOf(value) === -1);

In plain English, this returns all values in array a that are not in array b. This is actually used to uncheck all items: setChecked(not(checked, items)); 

The intersection function:

function intersection(a: number[], b: number[]) {
return a.filter((value) => b.indexOf(value) !== -1);

This returns a new array with all values in both arrays. It’s used with the buttons that shift items from one list to the other.

The union function:

function union(a: number[], b: number[]) {
return [...a, ...not(b, a)];

This returns a new array that contains all items in array a and all items in b that are not also in a. This is used in the Sandbox to keep any duplicates from occurring when the select all button is pressed.

Styling the Transfer List

I added elevation simply by wrapping the CardSelector in <Paper elevation={3}>. This gives a uniform box-shadow to the wrapped child.

I added a custom box-shadow to the <CardHeader>: boxShadow: “0px 4px 80px grey”. The 80px sets the blur value on the shadow, making the edges less sharp. I also had to add marginTop: 4 to the List component immediately adjacent to the <CardHeader>.

I liked the touches that these gave stylings gave to the transfer list. Additional custom styling could have been accomplished by targeting Card or CardHeader css classes. Take a look at some of those here.

This article about building a Material-UI Select component with Checkboxes will show you how to style Checkboxes, and here’s how to add them to the Treeview component.


Here’s more info on MUI Checkbox color and size.

View Code Sandbox here!

Expand your JavaScript knowledge with these 50 difficult JavaScript questions!


Share this post:

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.