Transfer List
A transfer list (or "shuttle") enables the user to move one or more list items between lists.
Basic transfer list
For completeness, this example includes buttons for "move all", but not every transfer list needs these.
import * as React from 'react'; import Grid from '@mui/material/Grid'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Checkbox from '@mui/material/Checkbox'; import Button from '@mui/material/Button'; import Paper from '@mui/material/Paper'; function not(a, b) { return a.filter((value) => b.indexOf(value) === -1); } function intersection(a, b) { return a.filter((value) => b.indexOf(value) !== -1); } export default function TransferList() { const [checked, setChecked] = React.useState([]); const [left, setLeft] = React.useState([0, 1, 2, 3]); const [right, setRight] = React.useState([4, 5, 6, 7]); const leftChecked = intersection(checked, left); const rightChecked = intersection(checked, right); const handleToggle = (value) => () => { const currentIndex = checked.indexOf(value); const newChecked = [...checked]; if (currentIndex === -1) { newChecked.push(value); } else { newChecked.splice(currentIndex, 1); } setChecked(newChecked); }; const handleAllRight = () => { setRight(right.concat(left)); setLeft([]); }; const handleCheckedRight = () => { setRight(right.concat(leftChecked)); setLeft(not(left, leftChecked)); setChecked(not(checked, leftChecked)); }; const handleCheckedLeft = () => { setLeft(left.concat(rightChecked)); setRight(not(right, rightChecked)); setChecked(not(checked, rightChecked)); }; const handleAllLeft = () => { setLeft(left.concat(right)); setRight([]); }; const customList = (items) => ( <Paper sx={{ width: 200, height: 230, overflow: 'auto' }}> <List dense component="div" role="list"> {items.map((value) => { const labelId = `transfer-list-item-${value}-label`; return ( <ListItem key={value} role="listitem" button onClick={handleToggle(value)} > <ListItemIcon> <Checkbox checked={checked.indexOf(value) !== -1} tabIndex={-1} disableRipple inputProps={{ 'aria-labelledby': labelId, }} /> </ListItemIcon> <ListItemText id={labelId} primary={`List item ${value + 1}`} /> </ListItem> ); })} <ListItem /> </List> </Paper> ); return ( <Grid container spacing={2} justifyContent="center" alignItems="center"> <Grid item>{customList(left)}</Grid> <Grid item> <Grid container direction="column" alignItems="center"> <Button sx={{ my: 0.5 }} variant="outlined" size="small" onClick={handleAllRight} disabled={left.length === 0} aria-label="move all right" > ≫ </Button> <Button sx={{ my: 0.5 }} variant="outlined" size="small" onClick={handleCheckedRight} disabled={leftChecked.length === 0} aria-label="move selected right" > > </Button> <Button sx={{ my: 0.5 }} variant="outlined" size="small" onClick={handleCheckedLeft} disabled={rightChecked.length === 0} aria-label="move selected left" > < </Button> <Button sx={{ my: 0.5 }} variant="outlined" size="small" onClick={handleAllLeft} disabled={right.length === 0} aria-label="move all left" > ≪ </Button> </Grid> </Grid> <Grid item>{customList(right)}</Grid> </Grid> ); }
Enhanced transfer list
This example exchanges the "move all" buttons for a "select all / select none" checkbox, and adds a counter.
Choices0/4 selected
Chosen0/4 selected
import * as React from 'react'; import Grid from '@mui/material/Grid'; import List from '@mui/material/List'; import Card from '@mui/material/Card'; import CardHeader from '@mui/material/CardHeader'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import ListItemIcon from '@mui/material/ListItemIcon'; import Checkbox from '@mui/material/Checkbox'; import Button from '@mui/material/Button'; import Divider from '@mui/material/Divider'; function not(a, b) { return a.filter((value) => b.indexOf(value) === -1); } function intersection(a, b) { return a.filter((value) => b.indexOf(value) !== -1); } function union(a, b) { return [...a, ...not(b, a)]; } export default function TransferList() { const [checked, setChecked] = React.useState([]); const [left, setLeft] = React.useState([0, 1, 2, 3]); const [right, setRight] = React.useState([4, 5, 6, 7]); const leftChecked = intersection(checked, left); const rightChecked = intersection(checked, right); const handleToggle = (value) => () => { const currentIndex = checked.indexOf(value); const newChecked = [...checked]; if (currentIndex === -1) { newChecked.push(value); } else { newChecked.splice(currentIndex, 1); } setChecked(newChecked); }; const numberOfChecked = (items) => intersection(checked, items).length; const handleToggleAll = (items) => () => { if (numberOfChecked(items) === items.length) { setChecked(not(checked, items)); } else { setChecked(union(checked, items)); } }; const handleCheckedRight = () => { setRight(right.concat(leftChecked)); setLeft(not(left, leftChecked)); setChecked(not(checked, leftChecked)); }; const handleCheckedLeft = () => { setLeft(left.concat(rightChecked)); setRight(not(right, rightChecked)); setChecked(not(checked, rightChecked)); }; const customList = (title, items) => ( <Card> <CardHeader sx={{ px: 2, py: 1 }} avatar={ <Checkbox onClick={handleToggleAll(items)} checked={numberOfChecked(items) === items.length && items.length !== 0} indeterminate={ numberOfChecked(items) !== items.length && numberOfChecked(items) !== 0 } disabled={items.length === 0} inputProps={{ 'aria-label': 'all items selected', }} /> } title={title} subheader={`${numberOfChecked(items)}/${items.length} selected`} /> <Divider /> <List sx={{ width: 200, height: 230, bgcolor: 'background.paper', overflow: 'auto', }} dense component="div" role="list" > {items.map((value) => { const labelId = `transfer-list-all-item-${value}-label`; return ( <ListItem key={value} role="listitem" button onClick={handleToggle(value)} > <ListItemIcon> <Checkbox checked={checked.indexOf(value) !== -1} tabIndex={-1} disableRipple inputProps={{ 'aria-labelledby': labelId, }} /> </ListItemIcon> <ListItemText id={labelId} primary={`List item ${value + 1}`} /> </ListItem> ); })} <ListItem /> </List> </Card> ); return ( <Grid container spacing={2} justifyContent="center" alignItems="center"> <Grid item>{customList('Choices', left)}</Grid> <Grid item> <Grid container direction="column" alignItems="center"> <Button sx={{ my: 0.5 }} variant="outlined" size="small" onClick={handleCheckedRight} disabled={leftChecked.length === 0} aria-label="move selected right" > > </Button> <Button sx={{ my: 0.5 }} variant="outlined" size="small" onClick={handleCheckedLeft} disabled={rightChecked.length === 0} aria-label="move selected left" > < </Button> </Grid> </Grid> <Grid item>{customList('Chosen', right)}</Grid> </Grid> ); }
Limitations
The component comes with a couple of limitations:
- It only works on desktop. If you have a limited amount of options to select, prefer the Autocomplete component. If mobile support is important for you, have a look at #27579.
- There are no high-level components exported from npm. The demos are based on composition. If this is important for you, have a look at #27579.