Skip to content

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:

html
<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 ​

ClassApplied to
tree-elementThe root ul.
tree-element-commonEvery element the widget creates.
tree-element-elementThe div that wraps a node's toggler and title.
tree-element-titleThe span with the node name.
tree-element-folderAn li for a node that has children.
tree-element-closedA closed folder — on the li and on its toggler.
tree-element-togglerThe open/close button.
tree-element-toggler-leftThe toggler when buttonLeft is true (the default).
tree-element-toggler-rightThe toggler when buttonLeft is false.
tree-element-title-folderThe title of a folder node.
tree-element-selectedThe li of a selected node.
tree-element-loadingA node — or the tree — that is fetching data.
tree-element-rtlThe root ul when rtl is true.
tree-element-dndThe root ul when dragAndDrop is true.
tree-element-ghostThe drop hint while dragging.
tree-element-insideThe drop hint for a drop inside a folder.
tree-element-movingThe node that is being dragged.
tree-element-borderThe 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:

js
new TreeElement({
  classPrefix: "my-tree",
  data,
  htmlElement,
});

The markup from the top of this page then looks like this:

html
<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.

js
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:

PropertyDefaultUsed for
--tree-element-title-color#1c4257The node titles.
--tree-element-toggler-color#333The open/close toggler.
--tree-element-toggler-hover-color#000The toggler under the mouse.
--tree-element-selected-backgroundlinear-gradient(#bee0f5, #89afca)The selected row.
--tree-element-selected-colorThe title colorThe title of the selected node.
--tree-element-selected-text-shadow0 1px 0 rgba(255, 255, 255, 0.7)The title of the selected node.
--tree-element-drop-hint-color#00fThe drop hints while dragging.
--tree-element-dragging-color#fffThe text of the node that is dragged.
--tree-element-dragging-background#000The background of the node that is dragged.
--tree-element-line-color#d5dbe1The 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:

css
@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:

css
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:

css
.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:

js
new TreeElement({
  closedIcon: "+",
  data,
  htmlElement,
  openedIcon: "−",
});

An element works too, which is how you use an svg or an icon font:

js
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:

js
new TreeElement({
  data,
  htmlElement,
  rtl: true,
});

You can also set it on the element:

html
<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:

js
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:

js
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:

js
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: