In it's simplest form, the Option List is a flat, selectable item list. With multi-selection, a Checkbox is incorporated.
Overview
Simple List
<State initial={[]}>
{([value, setValue]) => (
<OptionList
options={[
{value: 1, text: "Atlanta"},
{value: 2, text: "Boston"},
{value: 3, text: "Charlotte"},
{value: 4, text: "Detroit"},
{value: 5, text: "El Paso"}
]}
value={value}
onChange={(data) => setValue([data])}
/>
)}
</State>
<State initial={[]}>
{([value, setValue]) => (
<OptionList
options={[
{value: 1, text: "Fort Lauderdale"},
{value: 2, text: "Glendale"},
{value: 3, text: "Honolulu"},
{value: 4, text: "Indianapolis"},
{value: 5, text: "Jacksonville"}
]}
value={value}
onChange={(data, checked) =>
checked
? setValue(prev => [...prev, data])
: setValue(prev => prev.filter(item => item !== data))
}
multiple
/>
)}
</State>
Custom Content
Option Lists can have custom content inside individual options, regardless of what structure the Option List takes.
<State initial={[]}>
{([value, setValue]) => (
<OptionList
options={['Jane Doe', 'Dana Green', 'George Johnson'].map((name, index) => ({
value: index,
text: name,
content: (
<Stack alignItems="center" spacing={1}>
<Avatar name={name} autoColor size="s" />
<BodyText size="small">{name}</BodyText>
</Stack>
)
}))}
value={value}
onChange={data => setValue([data])}
/>
)}
</State>
const Example01 = () => {
const dummyFolderData = [{
content: (
<Stack alignItems="center" spacing={1}>
<Avatar name="Jane Doe" autoColor size="s" /><BodyText size="small">Jane Doe</BodyText>
</Stack>
),
value: 11,
options: [{
content: (
<Stack alignItems="center">
<BodyText size="small">Irrigation 01/02/20</BodyText>
<Button size="xsmall" fill="subtle" primary iconName="chevron_right" onClick={() => alert("New page about this specific item")} className="m-l-half" />
</Stack>
),
value: 21,
}, {
content: (
<Stack alignItems="center">
<BodyText size="small">Irrigation 03/04/20</BodyText>
<Button size="xsmall" fill="subtle" primary iconName="chevron_right" onClick={() => alert("New page about this specific item")} className="m-l-half" />
</Stack>
),
value: 22,
}],
}];
const [value, setValue] = React.useState([]);
const [collapseValue, setCollapseValue] = React.useState([11]);
const onChange = (data, checked, children) => {
const getValue = [data];
const getChildrenValues = (options) => {
options.map((option) => {
if (option.options) getChildrenValues(option.options);
if (option.disabled) return null;
return getValue.push(option.value);
});
};
if (children.length > 0) getChildrenValues(children);
if (checked)
setValue(prev => [...prev, ...getValue])
else
setValue(prev => prev.filter(item => !getValue.includes(item)));
};
const onExpand = (data) => {
collapseValue.includes(data)
? setCollapseValue(prev =>
prev.filter(item => item !== data)
)
: setCollapseValue(prev => [...prev, data]);
};
return (
<OptionList
options={dummyFolderData}
onChange={onChange}
onExpand={onExpand}
value={value}
collapseValue={collapseValue}
multiple
/>
);
}
render (Example01)
Flat Groups
The Option List can be paired with an Eyebrow to categorize a simple item list.
<State initial={[]}>
{([value, setValue]) => (
<OptionList
flatGroup
value={value}
onChange={(data) => setValue([data])}
options={[{
text: "Florida",
value: 1,
options: [
{value: 11, text: "Miami"},
{value: 12, text: "Orlando"},
{value: 13, text: "Tampa"}
]
}, {
text: "Minnesota",
value: 2,
options: [
{value: 2, text: "Minneapolis"},
{value: 3, text: "Rochester"},
{value: 4, text: "Saint Paul"}
]
}]}
/>
)}
</State>
<State initial={[]}>
{([value, setValue]) => (
<OptionList
flatGroup
multiple
value={value}
onChange={(data, checked) => checked
? setValue(prev => [...prev, data])
: setValue(prev => prev.filter(item => item !== data))
}
options={[{
text: "Montana",
value: 1,
options: [
{value: 11, text: "Billings"},
{value: 12, text: "Missoula"},
{value: 13, text: "Helena"}
]
}, {
text: "Colorado",
value: 2,
options: [
{value: 2, text: "Denver"},
{value: 3, text: "Fort Collins"},
{value: 4, text: "Boulder"}
]
}]}
/>
)}
</State>
Group Selection
In a flat group, individual groups can be given a select all and none control.
const Example01 = () => {
const flatData = [{
text: "HVAC Install",
value: 1,
options: [
{text: "Jane Doe", value: 11},
{text: "Jackie Robinson", value: 12},
{text: "Joseph Garcia", value: 13}
]
}, {
text: "Plumbing Replacement",
value: 2,
options: [
{text: "Dana Green", value: 2},
{text: "Isabella Martinez", value: 3},
{text: "George Johnson", value: 4}
]
}]
const [value, setValue] = React.useState([]);
const onChange = (data, checked) => {
checked
? setValue(prev => [...prev, data])
:setValue(prev => prev.filter(item => item !== data));
};
const onGroupSelectAll = (data) => {
const values = [...data.map(item => item.value)];
setValue(prev => [...prev, ...values]);
};
const onGroupSelectNone = (data) => {
const values = [...data.map(item => item.value)];
setValue(prev => prev.filter(item => !values.includes(item)));
};
return <OptionList options={flatData} value={value} onChange={onChange} flatGroup={{ onGroupSelectAll, onGroupSelectNone }} multiple />
}
render (Example01)
Secondary Actions
A secondary action can be performed on individual options. Example actions include editing, deleting, or navigating to more information. Secondary actions are only visible on hover.
const Example01 = () => {
const flatData = [{
text: "Raleigh",
value: 1,
secondaryAction: (<Button xsmall primary fill="subtle" onClick={() => alert("Action Clicked")}>Action</Button>)
}, {
text: "Sacramento",
value: 2,
secondaryAction: (<Button xsmall primary fill="subtle" onClick={() => alert("Action Clicked")}>Action</Button>)
}, {
text: "Tampa",
value: 3,
secondaryAction: (<Button xsmall primary fill="subtle" onClick={() => alert("Action Clicked")}>Action</Button>)
}];
const [value, setValue] = React.useState([]);
const onChange = (data) => setValue([data]);
return <OptionList options={flatData} value={value} onChange={onChange} />;
}
render (Example01)
Tree View
The tree view is a major variation of the Option List. It is used to represent hierarchical data, as understood by users. For example, an organizational chart, divisions within business units, or categorization of inventory items. Tree views work best when users perceive items as categorical.
const Example01 = () => {
const dummyData = [{
text: 'Plumbing',
value: 1,
collapsed: false,
options: [{
text: 'Install Materials',
value: 11,
options: [
{text: 'Disposer Waste', value: 111},
{text: 'Frostproof Hydrant', value: 112}
]
}, {
text: 'Service Materials',
value: 12,
options: [
{text: 'Fluidmaster Fill Valve', value: 121},
{text: 'Tailpiece Slip Joint', value: 122}
]
}]
}];
const [value, setValue] = React.useState([]);
const [collapseValue, setCollapseValue] = React.useState([1, 11, 12]);
const onChange = (data) => setValue([data]);
const onExpand = (data) => {
collapseValue.includes(data)
? setCollapseValue(prev => prev.filter(item => item !== data))
: setCollapseValue(prev => [...prev, data]);
};
return <OptionList options={dummyData} value={value} collapseValue={collapseValue} onChange={onChange} onExpand={onExpand} />
}
render (Example01)
const Example01 = () => {
const dummyData = [{
text: 'Plumbing',
value: 1,
collapsed: false,
options: [{
text: 'Install Materials',
value: 11,
options: [
{text: 'Disposer Waste', value: 111},
{text: 'Frostproof Hydrant', value: 112}
]
}, {
text: 'Service Materials',
value: 12,
options: [
{text: 'Fluidmaster Fill Valve', value: 121},
{text: 'Tailpiece Slip Joint', value: 122}
]
}]
}];
const [value, setValue] = React.useState([]);
const [collapseValue, setCollapseValue] = React.useState([1, 11, 12]);
const onChange = (data, checked, children) => {
const getValue = [data];
const getChildrenValues = (options) => {
options.map((option) => {
if (option.options) getChildrenValues(option.options);
if (option.disabled) return null;
return getValue.push(option.value);
});
};
if (children.length > 0) getChildrenValues(children);
if (checked)
setValue(prev => [...prev, ...getValue])
else
setValue(prev => prev.filter(item => !getValue.includes(item)));
};
const onExpand = (data) => {
collapseValue.includes(data)
? setCollapseValue(prev => prev.filter(item => item !== data))
: setCollapseValue(prev => [...prev, data]);
};
return (
<OptionList
multiple
options={dummyData}
value={value}
collapseValue={collapseValue}
onChange={onChange}
onExpand={onExpand}
/>
);
}
render (Example01)
Tree Selection Logic
The Option List is not opinionated on what is selected when a parent of options is selected. Different variants can be used depending on the design context.
Selecting parent selects all children
When a parent is just the sum of all its children, selecting the parent can select all children. This is the most common
const Example01 = () => {
const dummyData = [{
text: 'Option',
value: 1,
collapsed: false,
options: [{
text: 'Sub Option',
value: 11,
options: [
{text: 'Sub Sub Option', value: 111},
{text: 'Sub Sub Option', value: 112}
]
}, {
text: 'Sub Option',
value: 12,
options: [
{text: 'Sub Sub Option', value: 121},
{text: 'Sub Sub Option', value: 122}
]
}]
}, {
text: 'Option',
value: 2,
options: [{ text: 'Sub Option', value: 21 }]
}];
const [value, setValue] = React.useState([]);
const [collapseValue, setCollapseValue] = React.useState([1, 11, 12]);
const onChange = (data, checked, children) => {
const getValue = [data];
const getChildrenValues = (options) => {
options.map((option) => {
if (option.options) getChildrenValues(option.options);
if (option.disabled) return null;
return getValue.push(option.value);
});
};
if (children.length > 0) getChildrenValues(children);
if (checked)
setValue(prev => [...prev, ...getValue])
else
setValue(prev => prev.filter(item => !getValue.includes(item)));
};
const onExpand = (data) => {
collapseValue.includes(data)
? setCollapseValue(prev => prev.filter(item => item !== data))
: setCollapseValue(prev => [...prev, data]);
};
return <OptionList options={dummyData} value={value} collapseValue={collapseValue} onChange={onChange} onExpand={onExpand} multiple />;
}
render (Example01)
const Example01 = () => {
const dummyData = [{
text: 'Option',
value: 1,
collapsed: false,
options: [{
text: 'Sub Option',
value: 11,
options: [
{text: 'Sub Sub Option', value: 111},
{text: 'Sub Sub Option', value: 112}
]
}, {
text: 'Sub Option',
value: 12,
options: [
{text: 'Sub Sub Option', value: 121},
{text: 'Sub Sub Option', value: 122}
]
}]
}, {
text: 'Option',
value: 2,
options: [{ text: 'Sub Option', value: 21 }]
}];
const [value, setValue] = React.useState([]);
const [collapseValue, setCollapseValue] = React.useState([1, 11, 12]);
const onChange = (data, checked, children) => {
const getValue = [data];
const getChildrenValues = (options) => {
options.map((option) => {
if (option.options) getChildrenValues(option.options);
if (option.disabled) return null;
return getValue.push(option.value);
});
};
if (children.length > 0) getChildrenValues(children);
setValue(prev => [...getValue]);
};
const onExpand = (data) => {
collapseValue.includes(data)
? setCollapseValue(prev => prev.filter(item => item !== data))
: setCollapseValue(prev => [...prev, data]);
};
return <OptionList options={dummyData} value={value} collapseValue={collapseValue} onChange={onChange} onExpand={onExpand} />
}
render (Example01)
Selecting parent does not select children
There are times when a parent and child are independent selections from each other. This is useful when parent items can be configured distinctly from its children.
const Example01 = () => {
const dummyData = [{
text: 'Option',
value: 1,
collapsed: false,
options: [{
text: 'Sub Option',
value: 11,
options: [
{text: 'Sub Sub Option', value: 111},
{text: 'Sub Sub Option', value: 112}
]
}, {
text: 'Sub Option',
value: 12,
options: [
{text: 'Sub Sub Option', value: 121},
{text: 'Sub Sub Option', value: 122}
]
}]
}, {
text: 'Option',
value: 2,
options: [{ text: 'Sub Option', value: 21 }]
}];
const [value, setValue] = React.useState([]);
const [collapseValue, setCollapseValue] = React.useState([1, 11, 12]);
const onChange = (data, checked) => {
checked
? setValue(prev => [...prev, data])
: setValue(prev => prev.filter(item => item !== data));
};
const onExpand = (data) => {
collapseValue.includes(data)
? setCollapseValue(prev => prev.filter(item => item !== data))
: setCollapseValue(prev => [...prev, data]);
};
return <OptionList options={dummyData} value={value} collapseValue={collapseValue} onChange={onChange} onExpand={onExpand} multiple />
}
render (Example01)
const Example01 = () => {
const dummyData = [{
text: 'Option',
value: 1,
collapsed: false,
options: [{
text: 'Sub Option',
value: 11,
options: [
{text: 'Sub Sub Option', value: 111},
{text: 'Sub Sub Option', value: 112}
]
}, {
text: 'Sub Option',
value: 12,
options: [
{text: 'Sub Sub Option', value: 121},
{text: 'Sub Sub Option', value: 122}
]
}]
}, {
text: 'Option',
value: 2,
options: [{ text: 'Sub Option', value: 21 }]
}];
const [value, setValue] = React.useState([]);
const [collapseValue, setCollapseValue] = React.useState([1, 11, 12]);
const onChange = (data) => setValue([data]);
const onExpand = (data) => {
collapseValue.includes(data)
? setCollapseValue(prev => prev.filter(item => item !== data))
: setCollapseValue(prev => [...prev, data]);
};
return <OptionList options={dummyData} value={value} collapseValue={collapseValue} onChange={onChange} onExpand={onExpand} />
}
render (Example01)
Non-selectable Parents
Sometimes only childless options should be selected. For example, if parents represent folder structures, but the user needs to select a file.
const Example01 = () => {
const FolderIcon = () => {
return (
<Icon
className="m-r-1 c-neutral-100"
name="folder"
size="16px"
style={{verticalAlign: 'initial'}}
/>
);
};
const dummyFolderData = [{
content: <BodyText size="small"><FolderIcon />Pictures</BodyText>,
value: 'pictures',
readOnly: true,
options: [{
content: <BodyText size="small" className="m-l-1">dog.png</BodyText>,
value: 11,
}, {
content: <BodyText size="small" className="m-l-1">cat.jpg</BodyText>,
value: 12,
}]
}, {
content: <BodyText size="small"><FolderIcon />Documents</BodyText>,
value: 'documents',
readOnly: true,
options: [{
content: <BodyText size="small" className="m-l-1">worksheet.xls</BodyText>,
value: 21,
}, {
content: <BodyText size="small" className="m-l-1">README.md</BodyText>,
value: 22,
}]
}];
const [value, setValue] = React.useState([]);
const [collapseValue, setCollapseValue] = React.useState(['pictures']);
const onChange = (data) => setValue([data]);
const onExpand = (data) => {
collapseValue.includes(data)
? setCollapseValue(prev => prev.filter(item => item !== data))
: setCollapseValue(prev => [...prev, data]);
};
return (
<OptionList
options={dummyFolderData}
onChange={onChange}
onExpand={onExpand}
value={value}
collapseValue={collapseValue}
/>
)
}
render (Example01)
Indeterminate Visual
A tree can also take advantage of the Checkbox's indeterminate state to show partial selections of parents.
const Example01 = () => {
const memoizedData = React.useMemo(
() => [
{
text: 'Install Materials',
value: 11,
collapseControl: false,
options: [
{ text: 'Disposer Waste', value: 111 },
{ text: 'Frostproof Hydrant', value: 112 },
{ text: 'HVAC FGXB221', value: 113 },
],
},
],
[]
);
const [value, setValue] = React.useState([112]);
const [indeterminate, setIndeterminate] = React.useState([11]);
const memoizedParentValue = React.useMemo(() => {
const parentValue = memoizedData[0].value;
return parentValue;
}, [memoizedData]);
const memoizedChildValues = React.useMemo(() => {
const allChildValues = memoizedData[0].options.map(
(option) => option.value
);
return allChildValues;
}, [memoizedData]);
React.useEffect(() => {
const checkAllChildrenSelected = memoizedChildValues.every((i) =>
value.includes(i)
);
if (
value.some((i) => memoizedChildValues.includes(i)) &&
!checkAllChildrenSelected &&
!value.includes(memoizedParentValue)
) {
setIndeterminate([memoizedParentValue]);
} else setIndeterminate([]);
if (checkAllChildrenSelected && !value.includes(memoizedParentValue)) {
setValue((prev) => [...prev, memoizedParentValue]);
}
}, [memoizedChildValues, memoizedParentValue, value]);
const onChange = (data, checked, children) => {
const getValue = [data];
const getChildrenValues = (options) => {
options.map((option) => {
if (option.options) getChildrenValues(option.options);
if (option.disabled) return null;
return getValue.push(option.value);
});
};
if (children.length > 0) getChildrenValues(children);
if (checked) setValue((prev) => [...prev, ...getValue]);
else
setValue((prev) =>
prev.filter(
(item) =>
!getValue.includes(item) && item !== memoizedParentValue
)
);
};
return (
<div style={{ maxWidth: '400px' }}>
<OptionList
options={memoizedData}
multiple
onChange={onChange}
value={value}
indeterminateValue={indeterminate}
/>
</div>
);
}
render (Example01)
Tree View best practices
- Minimize the number of nested levels needed to represent the data.
- If the same data can be represented with 2 nested levels and 3, choose the 2 nested option.
- Categories should represent how a user best perceives a hierarchy. There are many different ways to categorize objects (e.g. is a video game console a toy or an electronic), it is important to understand how end-users perceive it.
Caution when using a Tree View
- Any usage of non-hierarchical data. Use a simpler Option List instead.
- User generated Trees. This data structure works best when data follows some sort of parent-child relation. User created structure may cause disorder and clutter that actively impairs a users ability to handle the data.
- When a child element might belong to multiple categories. This can create confusion for users in finding what they are looking for.
- In general, flat lists are easier to parse through than Trees. Designers and product managers should consider whether the hierarchy’s representation improves a user’s ability to work.
- For a simple list, particularly one tied to Forms, consider the Checkbox or Radio.
- For a selection list that needs to be in an overlay, use the Select.
- For a list of navigational items, consider using the Side Nav.