Building Pure CSS Trees (part 1)

Have you ever wanted to represent some hierarchical data on a webpage as a tree? In this series of posts, we are going to build a CSS-only solution for rendering hierarchical trees.

The HTML for our hierarchical data will be structured as nested lists:

<ul class="tree">
<li>
<span><code>1</code></span>
<ul>
<li>
<span><code>1.1</code></span>
</li>
<li>
<span><code>1.2</code></span>
<ul>
<li>
<span><code>1.2.1</code></span>
</li>
<li>
<span><code>1.2.2</code></span>
</li>
<li>
<span><code>1.2.3</code></span>
</li>
</ul>
</li>
</ul>
</li>
</ul>

Let’s begin by clearing the list of any default list styling:

.tree {
list-style: none;
 
&,
* {
margin: 0;
padding: 0;
}
}

See the Pen css-tree__1 by Stephen Margheim (@smargh) on CodePen.

Flexbox is going to be the heart of our CSS-only implementation. It will give us the power and the flexibility to take our nested lists HTML and render it as a hierarchical tree in a number of different orientations. Initially, however, let’s build a tree that renders along the horizontal axis:

.tree {
// ...
 
li {
display: flex;
flex-direction: row;
align-items: center;
}
}

This CSS declares that every node is going to be a flex container where the orientation is left-to-right along the horizontal axis and the flex children will be centered along their respective vertical axes.

See the Pen css-tree__2 by Stephen Margheim (@smargh) on CodePen.

Before we start building the connectors, let’s quickly add some basic styling for the nodes so that we can better see our connectors as we build them:

.tree {
// ...
 
span {
border: 1px solid;
text-align: center;
padding: 0.33em 0.66em;
}
}

See the Pen css-tree__3 by Stephen Margheim (@smargh) on CodePen.

The first connector that we want to build is the line from parent-to-children. Given that we are starting with a simple left-to-right, horizontal tree, this connector will extend out to the right of any parent node.

.tree {
// ...
 
ul,
ol {
padding-left: 2vw;
position: relative;
 
// [connector] parent-to-children
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
border-top: 1px solid;
width: 2vw;
}
}
}

See the Pen css-tree__4 by Stephen Margheim (@smargh) on CodePen.

This CSS declares that any nested lists (uls or ols that are descendants of the .tree list) will have a 1-pixel line, the width of which will be 2% of the viewport width, that is vertically centered and horizontally aligned to the far-left.

The goal is to have a line that comes out of the center of any parent node; notably, however, we do not put the border on the parent li element, but on the ul or ol element that represents the set of children for the parent node. We do this because we will need both the :before and :after pseudo-elements of the lis to build the connectors for the children.

Let’s start building those connectors now.

.tree {
// ...
 
li {
// ...
 
position: relative;
padding-left: 2vw;
 
// [connector] child-to-parent
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
border-top: 1px solid;
width: 2vw;
}
}
}

See the Pen css-tree__5 by Stephen Margheim (@smargh) on CodePen.

This CSS is simple insofar as it is a direct re-use of the CSS used for the nested uls and ols. For every li element, we ensure that it has a 1-pixel line, the width of which will be 2% of the viewport width, that is vertically centered and horizontally aligned to the far-left.

You should note immediately one issue: the root node has a child-to-parent connector, though it has no parent. Let us remedy that first:

.tree {
// ...
 
> li {
padding-left: 0;
 
&::before,
&::after {
display: none;
}
}
}

See the Pen css-tree__6 by Stephen Margheim (@smargh) on CodePen.

This ensures that the root node (the direct child of the .tree list) has no left padding and no child-to-parent connector.

The only connector left is the vertical line that groups a set of siblings together. In order to create such a line, we need to isolate the first child of a set, the last child, and any middle children. For the middle children, we simple draw a vertical line that is the same height as the child, horizontally aligned to the far-left. For the first child, we want to draw a vertical line that is half the height as the child and that is drawn beneath the child-to-parent connector. Finally, for the last child, we want another half-height vertical line, but this time it is drawn above the child-to-parent connector. For this task, we will use the :after pseudo-element:

.tree {
// ...
 
li {
// ...
 
// [connector] sibling-to-sibling
&::after {
content: "";
position: absolute;
left: 0;
border-left: 1px solid;
}
// [connector] sibling-to-sibling:last-child
&:last-of-type::after {
height: 50%;
top: 0;
}
// [connector] sibling-to-sibling:first-child
&:first-of-type::after {
height: 50%;
bottom: 0;
}
// [connector] sibling-to-sibling:middle-child(ren)
&:not(:first-of-type):not(:last-of-type)::after {
height: 100%;
}
}
}

See the Pen css-tree__7 by Stephen Margheim (@smargh) on CodePen.

The only major bit we will add for now is some vertical spacing between children nodes by adding

padding-top: 0.5vh;
padding-bottom: 0.5vh;

to the li selector.

See the Pen css-tree__8 by Stephen Margheim (@smargh) on CodePen.


With 78 lines of CSS, we have rendered a nested list as a hierarchical tree graph. In following posts we will extend this CSS to allow for more flexibility and robustness.


All posts in this series