The <cbx-tree>
element is a web component for building tree-like hierarchic lists with checkable items. Tree items in the <cbx-tree>
element are collapsible if they have nested subtrees. Every item is equipped with a checkbox which can be in one of the following states:
- checked: the item and all its children are checked,
- unchecked: the item and all its children are unchecked,
- indeterminate: the item is technically unchecked but some of its children are checked.
If you use a bundler in your project, install cbx-tree as a dependency:
npm install cbx-tree
Now you may import it wherever it’s needed:
import 'cbx-tree';
If you don’t use bundlers, just import the component as a module in your HTML files:
<script type="module" src="https://unpkg.com/cbx-tree"></script>
or in ES modules:
import 'https://unpkg.com/cbx-tree';
There are two ways to feed initial tree data to the <cbx-tree>
component.
The first way is to provide tree data directly in HTML by adding JSON content as follows:
<cbx-tree name="reading-list[]">
<script type="application/json">
[
{
"title": "Epic poetry",
"value": "category-123",
"icon": "/icons/epic-icon.svg",
"children": [
{
"title": "Ancient Greek poems",
"value": "category-179",
"icon": "/icons/greek-icon.svg",
"children": [
{
"title": "Iliad",
"value": "book-10",
"icon": "/icons/manuscript-icon.svg",
"checked": true
},
{
"title": "Odyssey",
"value": "book-11",
"icon": "/icons/manuscript-icon.svg",
"checked": true
}
]
},
{
"title": "Ancient Mesopotamian poems",
"value": "category-151",
"icon": "/icons/mesopotamian-icon.svg",
"collapsed": true,
"children": [
{
"title": "Epic of Gilgamesh",
"value": "book-8",
"icon": "/icons/clay-tablet-icon.svg"
}
]
}
]
}
]
</script>
</cbx-tree>
Note
Similarly to the <textarea>
content, the data you provide in HTML is only used as a default value. In other words, dynamic updates of the HTML content don’t affect the current tree. To update the tree dynamically, one should use the JavaScript API provided by the component.
The second option is to fill the initial tree programmatically using the setData()
method.
HTML:
<cbx-tree name="reading-list[]"></cbx-tree>
JavaScript:
customElements.whenDefined('cbx-tree').then(() => {
const readingList = document.querySelector('[name="reading-list[]"]');
readingList.setData([
{
title: 'Epic poetry',
value: 'category-123',
icon: '/icons/epic-icon.svg',
children: [
{
title: 'Ancient Greek poems',
value: 'category-179',
icon: '/icons/greek-icon.svg',
children: [
{
title: 'Iliad',
value: 'book-10',
icon: '/icons/manuscript-icon.svg',
checked: true,
},
{
title: 'Odyssey',
value: 'book-11',
icon: '/icons/manuscript-icon.svg',
checked: true,
},
],
},
{
title: 'Ancient Mesopotamian poems',
value: 'category-151',
icon: '/icons/mesopotamian-icon.svg',
collapsed: true,
children: [
{
title: 'Epic of Gilgamesh',
value: 'book-8',
icon: '/icons/clay-tablet-icon.svg',
},
],
},
],
},
]);
});
Note
JavaScript API of the <cbx-tree>
component becomes fully functional as soon as the element is registered and defined. To stay on the safe side, it’s worth using the whenDefined()
guard as shown in the example above.
As shown in the examples above, the tree is initialised with an array of objects representing the tree’s root items. Each item can have children forming a nested subtree. The table below provides information about the properties that can be specified for tree items at any nesting level.
Property | Type | Required | Description |
---|---|---|---|
title |
string | yes | Text label of the tree item |
value |
string¹ | yes | Internal value identifying the checked item in the submitted data |
icon |
string | no | Item icons’s URL or SVG icon code |
checked |
boolean | no | Initial state of the item selection |
collapsed |
boolean | no | Whether a nested subtree is collapsed initially |
children |
array or null ² |
no | Nested subtree items |
¹ Must be unique within the entire tree.
² The value null
of the children
property is used for on-demand loading of the subtree.
This element includes the global attributes.
Applying this Boolean attribute turns all interactive controls within the tree into the disabled state. Items in the disabled tree cannot be collapsed or expanded by the user, and states of the checkboxes cannot be changed via the GUI.
A mandatory attribute name
is used by the <cbx-tree>
component to construct data to be submitted with the form. Since the widget contains multiple checkable items, it may be a good idea to use a name with square brackets appended. This notation allows some server-side frameworks treat submitted data as an array.
<cbx-tree name="reading-list[]"></cbx-tree>
By default, items in the <cbx-tree>
component grab focus and get highlighted when pointer hovers over them, similarly to options in the <select>
element’s dropdown. A Boolean attribute nohover
makes the <cbx-tree>
component deactivate this behaviour, so that items only become selected when clicked or focused by keyboard navigation (similarly to options in a <select>
with the multiple
attribute specified).
<cbx-tree name="reading-list[]" nohover></cbx-tree>
The CbxTree
interface also inherits properties from its parent, HTMLElement.
Validation-related properties validity
, validationMessage
, and willValidate
are transparently exposed from the underlying ElementInternals
object which allows the <cbx-tree>
element participate in form validation.
Reflects the value of the element’s disabled
attribute.
The read-only property that references the HTMLFormElement
associated with this element.
A FormData
object which contains key-value pairs of the currently checked items in the tree. Note that the element’s name
attribute is used for all keys, so the FormData
object represents an array of checked values. The property is read-only.
const readingList = document.querySelector('[name="reading-list[]"]');
console.log('Checked values:', readingList.formData.getAll('reading-list[]'));
Reflects the value of the element’s name
attribute.
Reflects the value of the element’s nohover
attribute.
The subtreeProvider
property is used in cases where on-demand subtree loading is required. If your initial tree doesn’t contain data for some nested subtrees, you may define your custom function for subtree generation/fetching which will be called when the user expands the target item for the first time.
Important
The items that allow on-demand loading, should have their children
property set to null
initially.
The custom subtree provider is a function that accepts the value of the target item as its argument and returns a promise that resolves with an array representing a subtree data for this specific item.
customElements.whenDefined('cbx-tree').then(() => {
const readingList = document.querySelector('[name="reading-list[]"]');
readingList.subtreeProvider = async (itemValue) => {
const response = await fetch(`/reading-list/items/${itemValue}`);
return (await response.json()).children;
};
});
A read-only property provided for consistency with browser-provided form controls. The same as Element.localName
.
The CbxTree
interface also inherits methods from its parent, HTMLElement.
Validation-related methods checkValidity()
, reportValidity()
, and setValidity()
are transparently exposed from the underlying ElementInternals
object which allows the <cbx-tree>
element participate in form validation.
This method can be used to “filter” the tree by hiding those items that don’t meet custom criteria. The method accepts a single argument, a predicate function. The predicate is passed an object argument with item’s title
and value
as properties, and the return value must be true
if the item passes the filter and false
otherwise. It should be noted that if an item passes the filter, its descendants remain visible even if they themselves don’t satisfy the filtering condition.
const readingList = document.querySelector('[name="reading-list[]"]');
const filterInput = document.getElementById('filter');
filterInput.addEventListener('input', () => {
const query = filterInput.value.trim().toLocaleLowerCase();
const predicate = query.length ? ({title}) => title.toLocaleLowerCase().includes(query) : () => true;
readingList.filter(predicate);
});
The setData()
method is used for complete overwriting and rerendering the entire tree. It accepts a single argument, a new tree data. All existing changes will be lost and replaced by the newly provided data after calling this method. See an example in the Usage notes section.
Use this method for dynamic expansion or collapsing of all items in the tree. The method accepts an optional boolean argument, isExpanding
, which controls whether items should be expanded (true
) or collapsed (false
).
const readingList = document.querySelector('[name="reading-list[]"]');
readingList.toggle(false); // collapse all
Note that this method doesn’t expand items that have on-demand loading behavior. Also, programmatic toggling doesn’t trigger the cbxtreetoggle
event.
This method can be used to check or uncheck all the items in the tree. The method accepts an optional boolean argument, checked
, which controls whether items should be checked (true
) or unchecked (false
).
const readingList = document.querySelector('[name="reading-list[]"]');
readingList.toggleChecked(true); // check all
Note that programmatic checking of the items doesn’t trigger the cbxtreechange
event.
Returns the current state of the tree in the same format as the array used for tree initialisation. This method allows for JSON serialisation of the control state.
const readingList = document.querySelector('[name="reading-list[]"]');
console.log('Tree data:', JSON.stringify(readingList, null, 2));
The cbxtreechange
custom event is fired when the user changes the state of a checkbox in the tree. A complete information on the tree selection state is available as a FormData
object through the detail
property of the event instance.
const readingList = document.querySelector('[name="reading-list[]"]');
readingList.addEventListener('cbxtreechange', (e) => {
const selectionData = e.detail; // FormData instance
console.log('Selected books & categories:', ...selectionData.values());
});
The cbxtreetoggle
custom event is fired when the user clicks a toggle button to expand or collapse a subtree under one of the tree items. The detail
property of the event instance provides additional information about the target item:
Property | Description |
---|---|
title |
Title of the target item |
value |
Value of the target item |
newState |
New state of the target item (either expanded or collapsed ) |
const readingList = document.querySelector('[name="reading-list[]"]');
readingList.addEventListener('cbxtreetoggle', (e) => {
const {title, value, newState} = e.detail;
console.log(`Item “${title}” (${value}) is now ${newState}`);
});
The <cbx-tree>
element provides a few CSS custom properties (variables) that you can override for your needs.
Variable | Data type | Description |
---|---|---|
--cbx-tree-toggle-closed-mask |
<url> ¹ |
Mask image for the toggle button in the collapsed state |
--cbx-tree-toggle-open-mask |
<url> |
Mask image for the toggle button in the expanded state |
--cbx-tree-toggle-pending-mask |
<url> |
Mask image for the toggle button in the pending state |
--cbx-tree-label-focus-bg |
<color> ² |
Background color for the highlighted item’s label |
--cbx-tree-label-focus-fg |
<color> |
Text color for the highlighted item’s label |
--cbx-tree-nesting-indent |
<length> ³ |
Indentation size for nested subtrees |
¹ https://developer.mozilla.org/en-US/docs/Web/CSS/url_value
² https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
³ https://developer.mozilla.org/en-US/docs/Web/CSS/length
In the following example, item toggle button’s mask is changed from the default arrow to “+/−” icons:
cbx-tree {
--cbx-tree-toggle-closed-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="14" height="14"><path d="M3 7L11 7M7 3L7 11" stroke="black"/></svg>');
--cbx-tree-toggle-open-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="14" height="14"><path d="M3 7L11 7" stroke="black"/></svg>');
}
Additionally, parts of the <cbx-tree>
element can be directly styled through the ::part()
pseudo-element.
Caution
Directly styling the inner parts of the tree is (to some extent) an advanced technique that comes with the risk of breaking the tree’s UI. Use it as a last resort if the desired result cannot be achieved with regular CSS inheritance.
The available ::part()
pseudo-elements are listed in the following table and are shown in the picture below.
Pseudo-element | Matched parts |
---|---|
::part(tree) |
The root tree and any nested subtree |
::part(item) |
Any individual item of a tree/subtree |
::part(toggle) |
Item toggle buttons |
::part(label) |
Wrappers around any item’s checkbox, icon, and title |
::part(checkbox) |
Any item’s checkbox |
::part(icon) |
Any item’s icon |
::part(title) |
Any item’s title |
cbx-tree::part(title) {
transition: scale 0.2s ease-in-out;
transform-origin: 0 50%;
}
cbx-tree::part(title):hover {
scale: 1.1;
}