Styling
The widget needs tree_element.css to look like a tree: it removes the list bullets, indents the levels, hides closed folders and draws the drag-and-drop hints. Everything else — colors, fonts, spacing — is meant to be overridden.
The stylesheet is generated from css/tree_element.postcss by pnpm production.
Markup
A tree renders as nested lists:
<ul class="tree-element-common tree-element" role="tree">
<li class="tree-element-common tree-element-folder" role="none">
<div class="tree-element-element tree-element-common" role="none">
<span
class="tree-element-toggler tree-element-common tree-element-toggler-left"
aria-hidden="true"
><svg …>…</svg></span
>
<span
class="tree-element-title tree-element-common tree-element-title-folder tree-element-title-button-left"
role="treeitem"
>parent</span
>
</div>
<ul class="tree-element-common" role="group">
<li class="tree-element-common" role="none">
<div class="tree-element-element tree-element-common" role="none">
<span
class="tree-element-title tree-element-common tree-element-title-button-left"
>child</span
>
</div>
</li>
</ul>
</li>
</ul>The children of a closed folder are not in the markup: the nested ul is added the first time the folder is opened, and stays in place when it is closed again.
Note that all selectors in the stylesheet are nested under ul.tree-element, so your own rules should be too — or be specific enough to win.
Class names
| Class | Applied to |
|---|---|
tree-element | The root ul. |
tree-element-common | Every element the widget creates. |
tree-element-element | The div that wraps a node's toggler and title. |
tree-element-title | The span with the node name. |
tree-element-folder | An li for a node that has children. |
tree-element-closed | A closed folder — on the li and on its toggler. |
tree-element-toggler | The open/close button. |
tree-element-toggler-left | The toggler when buttonLeft is true (the default). |
tree-element-toggler-right | The toggler when buttonLeft is false. |
tree-element-title-folder | The title of a folder node. |
tree-element-selected | The li of a selected node. |
tree-element-loading | A node — or the tree — that is fetching data. |
tree-element-rtl | The root ul when rtl is true. |
tree-element-dnd | The root ul when dragAndDrop is true. |
tree-element-ghost | The drop hint while dragging. |
tree-element-inside | The drop hint for a drop inside a folder. |
tree-element-moving | The node that is being dragged. |
tree-element-border | The border drawn around a folder that is being dropped into. |
Changing the class names
All classes start with tree-element. The classPrefix option changes that prefix:
new TreeElement({
classPrefix: "my-tree",
data,
htmlElement,
});The markup from the top of this page then looks like this:
<ul class="my-tree-common my-tree" role="tree">
<li class="my-tree-common my-tree-folder" role="none">
<div class="my-tree-element my-tree-common" role="none">
<span
class="my-tree-toggler my-tree-common my-tree-toggler-left"
aria-hidden="true"
><svg …>…</svg></span
>
<span
class="my-tree-title my-tree-common my-tree-title-folder my-tree-title-button-left"
role="treeitem"
>parent</span
>
</div>
<ul class="my-tree-common" role="group">
<li class="my-tree-common" role="none">
<div class="my-tree-element my-tree-common" role="none">
<span class="my-tree-title my-tree-common my-tree-title-button-left"
>child</span
>
</div>
</li>
</ul>
</li>
</ul>Two of the classes are not <prefix>-<something>, so they have an option of their own: treeClassName for the class of the root ul — classPrefix by default — and commonClassName for the class that every element gets — <classPrefix>-common by default.
new TreeElement({
commonClassName: "tree-node",
data,
htmlElement,
treeClassName: "tree",
});Note that tree_element.css has the default prefix baked in: a tree with another prefix is unstyled until you provide your own css. Take css/tree_element.postcss as the starting point — replacing tree-element in it with your prefix gives you the same stylesheet for your class names.
Colors
The colors are css custom properties. Set them on :root, or on an element around the tree to change one tree:
| Property | Default | Used for |
|---|---|---|
--tree-element-title-color | #1c4257 | The node titles. |
--tree-element-toggler-color | #333 | The open/close toggler. |
--tree-element-toggler-hover-color | #000 | The toggler under the mouse. |
--tree-element-selected-background | linear-gradient(#bee0f5, #89afca) | The selected row. |
--tree-element-selected-color | The title color | The title of the selected node. |
--tree-element-selected-text-shadow | 0 1px 0 rgba(255, 255, 255, 0.7) | The title of the selected node. |
--tree-element-drop-hint-color | #00f | The drop hints while dragging. |
--tree-element-dragging-color | #fff | The text of the node that is dragged. |
--tree-element-dragging-background | #000 | The background of the node that is dragged. |
--tree-element-line-color | #d5dbe1 | The lines from a folder to its children. |
The tree lines are decoration, so they are light. Set --tree-element-line-color to transparent to hide them.
The defaults meet the WCAG AA contrast minimums on a white page: the title has at least 4.6:1, also on the darkest part of the selected row. Check the contrast when you change them.
The stylesheet has no dark theme, because the tree can't know the background of your page. A dark theme for a page with a #121212 background:
@media (prefers-color-scheme: dark) {
:root {
--tree-element-title-color: #cfe3f1;
--tree-element-toggler-color: #a8b3bd;
--tree-element-toggler-hover-color: #fff;
--tree-element-selected-background: #1f3a52;
--tree-element-selected-text-shadow: none;
--tree-element-drop-hint-color: #6ea8ff;
--tree-element-line-color: #3a4651;
}
}Overriding styles
For anything that isn't a color, override the rules of the stylesheet. Indentation, for example:
ul.tree-element ul.tree-element-common {
margin-left: 24px;
}The tree line is the left border of that list, so it moves along with the margin.
Toggler size
--tree-element-toggler-size is the width of the toggler. The stylesheet derives the rest from it: the indentation is half of it, so the tree line is under the center of the toggler, and the titles of nodes without children are indented by all of it, so they line up with the titles of folders.
The default is max(24px, 1.5em): 24px, the minimum size for something you click, up to a 16px font, then it grows with the font. The space around the icon separates the toggler from the title, so it grows along. Set it when your icon is wider:
.my-tree {
--tree-element-toggler-size: 32px;
}Folder icons
The togglers are small svg triangles by default, pointing right for a closed folder and down for an open one. They are sized in em, so they follow the font size, and take the color of --tree-element-toggler-color. Replace them with closedIcon and openedIcon, which take an html string or an element:
new TreeElement({
closedIcon: "+",
data,
htmlElement,
openedIcon: "−",
});An element works too, which is how you use an svg or an icon font:
const icon = document.createElement("i");
icon.className = "fa fa-folder";
new TreeElement({
closedIcon: icon,
data,
htmlElement,
});Put the toggler after the title instead of before it with buttonLeft: false:
Right to left
rtl: true mirrors the tree — the indentation, the togglers and the drag-and-drop hints all move to the other side — and flips the default closed icon to point left:
new TreeElement({
data,
htmlElement,
rtl: true,
});You can also set it on the element:
<div id="tree1" data-rtl="true"></div>The mirroring comes from the tree-element-rtl class that the option adds to the root ul, so it is all in tree_element.css: if you replace the stylesheet, carry those rules over.
Customizing the markup
onCreateLi is called for every rendered node, with the node, its li element and whether it is selected. A node inside a closed folder is rendered — and onCreateLi called — when that folder is opened. Use it to add your own content:
new TreeElement({
data,
htmlElement,
onCreateLi: (node, li, isSelected) => {
const title = li.querySelector(".tree-element-title");
if (node.count) {
const badge = document.createElement("span");
badge.className = "count";
badge.textContent = String(node.count);
title.after(badge);
}
},
});Keep in mind that the li is rebuilt whenever the node is refreshed, so onCreateLi has to be able to run again — build the markup from the node data rather than mutating what is already there.
Empty folders
A node with children: [] is rendered as a leaf. Set showEmptyFolder: true to render it as a folder that can be opened and closed:
new TreeElement({
data: [
{ name: "empty folder", id: 1, children: [] },
{ name: "leaf", id: 2 },
],
htmlElement,
showEmptyFolder: true,
});Animation
Opening and closing folders slides by default. Turn it off with slide: false, or change the speed with animationSpeed, which takes "fast", "slow" or a number of milliseconds:
new TreeElement({
animationSpeed: 200,
data,
htmlElement,
});When the user prefers reduced motion — prefers-reduced-motion: reduce, set in the system settings — the default animationSpeed is 0, so folders open and close instantly. Setting animationSpeed yourself overrides this.
This tree has slide: false, so folders open and close instantly: