https://iamkate.com/code/tree-views/
* Code
* Data
* Games
Tree views in CSS
A tree view (collapsible list) can be created using only HTML and CSS
, without the need for JavaScript. Accessibility software will see
the tree view as lists nested inside disclosure widgets, and the
standard keyboard interaction is supported automatically.
* Giant planets
+ Gas giants
o Jupiter
o Saturn
+ Ice giants
o Uranus
o Neptune
The HTML
We begin with the HTML for simple nested lists:
1
2 -
3 Giant planets
4
5 -
6 Gas giants
7
11
12 -
13 Ice giants
14
18
19
20
21
We then add a class to the outermost element, and for each list
item that contains a nested list, we put the contents of the list
item inside and elements, using the open
attribute to control which nested lists are initially expanded:
1
2 -
3
4 Giant planets
5
6 -
7
8 Gas giants
9
13
14
15 -
16
17 Ice giants
18
22
23
24
25
26
27
Without any styling, this HTML produces:
* Giant planets
+ Gas giants
o Jupiter
o Saturn
+ Ice giants
o Uranus
o Neptune
The browser implements the element as a disclosure widget,
giving the ability to expand and collapse the nested lists, but the
combination of bullet points and disclosure arrows produces a
confusing user interface.
Custom properties
There are two dimensions that affect the layout of the tree view: the
spacing between lines (which equals the line height of the text) and
the radius of the markers. We begin by creating CSS custom properties
for these dimensions:
1 .tree{
2 --spacing : 1.5rem;
3 --radius : 10px;
4 }
While we would usually use relative units to scale user interface
controls based on the text size, for the markers this can lead to
controls that are too small or excessively large, so we instead use a
reasonable fixed size.
Padding
We then style the list items and nested lists to make space for the
lines and markers:
6 .tree li{
7 display : block;
8 position : relative;
9 padding-left : calc(2 * var(--spacing) - var(--radius) - 2px);
10 }
11
12 .tree ul{
13 margin-left : calc(var(--radius) - var(--spacing));
14 padding-left : 0;
15 }
Line 7 removes the bullet points from list items. Line 8 establishes
a new stacking context and containing block that we will use to
position the lines and markers.
Line 9 indents the list items. The indentation is equal to twice the
spacing, minus the marker radius, minus the two-pixel line width. The
result is that the text in a list item will align with the left side
of the marker below it.
Line 13 uses a negative margin to compensate for the indentation
introduced by line 9, ensuring nested lists are indented by only the
desired spacing. Line 14 removes the default padding that browsers
apply to lists.
On a tree view with all nested lists initially expanded, applying
this styling produces:
* Giant planets
+ Gas giants
o Jupiter
o Saturn
+ Ice giants
o Uranus
o Neptune
Vertical lines
Next we add the vertical lines that form part of the lines joining
the marker of each list item to the markers of its nested lists:
17 .tree ul li{
18 border-left : 2px solid #ddd;
19 }
20
21 .tree ul li:last-child{
22 border-color : transparent;
23 }
We use a border to create the line, and hide it on the final item in
each list as the line shouldn't continue past this item's marker.
Making the border transparent, rather than removing it completely,
avoids the need to increase the padding to compensate.
Applying this styling produces:
* Giant planets
+ Gas giants
o Jupiter
o Saturn
+ Ice giants
o Uranus
o Neptune
Horizontal lines
We use generated content to add the horizontal lines that join the
vertical lines to the markers of each list item:
25 .tree ul li::before{
26 content : '';
27 display : block;
28 position : absolute;
29 top : calc(var(--spacing) / -2);
30 left : -2px;
31 width : calc(var(--spacing) + 2px);
32 height : calc(var(--spacing) + 1px);
33 border : solid #ddd;
34 border-width : 0 0 2px 2px;
35 }
This code also creates short vertical lines, as the vertical lines
created previously don't extend all the way to the markers at their
top and bottom ends.
Lines 26 and 27 generate a block, and lines 28 to 30 position it to
start at the midpoint of the preceding line of text, overlapping the
vertical line to its left.
Lines 31 and 32 set the size of the block. It needs to be two pixels
wider than the spacing as it overlaps the vertical line to its left,
and one pixel taller than the spacing as half the width of the
horizontal line lies below the midpoint of the line of text. Note
that we are assuming the use of border box sizing, so these
dimensions include the border.
Lines 33 and 34 create a border on the left and bottom sides of the
block.
Applying this styling produces:
* Giant planets
+ Gas giants
o Jupiter
o Saturn
+ Ice giants
o Uranus
o Neptune
Summaries
Next we remove the default styling from summaries:
37 .tree summary{
38 display : block;
39 cursor : pointer;
40 }
41
42 .tree summary::marker,
43 .tree summary::-webkit-details-marker{
44 display : none;
45 }
46
47 .tree summary:focus{
48 outline : none;
49 }
50
51 .tree summary:focus-visible{
52 outline : 1px dotted #000;
53 }
Lines 38 and 44 remove the disclosure arrows. Line 44 is needed for
Safari, with the two selectors on lines 42 and 43 covering different
versions of the browser. Line 39 changes the cursor to indicate that
the summary can be clicked to interact with it.
Safari shows a focus indicator around summaries, even when using a
pointer rather than keyboard navigation, so we remove the focus
styling on line 48 and then use the :focus-visible pseudo-class to
add it back for visitors using keyboard navigation on line 52.
Applying this styling produces:
* Giant planets
+ Gas giants
o Jupiter
o Saturn
+ Ice giants
o Uranus
o Neptune
Markers
We use generated content again to create the markers:
55 .tree li::after,
56 .tree summary::before{
57 content : '';
58 display : block;
59 position : absolute;
60 top : calc(var(--spacing) / 2 - var(--radius));
61 left : calc(var(--spacing) - var(--radius) - 1px);
62 width : calc(2 * var(--radius));
63 height : calc(2 * var(--radius));
64 border-radius : 50%;
65 background : #ddd;
66 }
Note that we generate markers both for - elements (for list items
that don't contain nested lists) and for
summary::before{
78 content : '-';
79 }
Lines 69 and 78 show plus and minus signs in the buttons. Note that
we use a true minus sign (-) rather than a hyphen (-) as this matches
the appearance of the plus sign, whereas in most fonts the hyphen is
narrower and lower.
Line 70 causes the button to be displayed on top of the marker
created previously. As the marker was created using ::after it would
otherwise be displayed on top of the button.
Lines 71 to 74 set the button's colours and centre its text, with
this particular font requiring a 2px vertical adjustment on line 73.
Applying this styling produces the finished tree view:
* Giant planets
+ Gas giants
o Jupiter
o Saturn
+ Ice giants
o Uranus
o Neptune
The finished code
Combining all of the above leads to the finished code:
1 .tree{
2 --spacing : 1.5rem;
3 --radius : 10px;
4 }
5
6 .tree li{
7 display : block;
8 position : relative;
9 padding-left : calc(2 * var(--spacing) - var(--radius) - 2px);
10 }
11
12 .tree ul{
13 margin-left : calc(var(--radius) - var(--spacing));
14 padding-left : 0;
15 }
16
17 .tree ul li{
18 border-left : 2px solid #ddd;
19 }
20
21 .tree ul li:last-child{
22 border-color : transparent;
23 }
24
25 .tree ul li::before{
26 content : '';
27 display : block;
28 position : absolute;
29 top : calc(var(--spacing) / -2);
30 left : -2px;
31 width : calc(var(--spacing) + 2px);
32 height : calc(var(--spacing) + 1px);
33 border : solid #ddd;
34 border-width : 0 0 2px 2px;
35 }
36
37 .tree summary{
38 display : block;
39 cursor : pointer;
40 }
41
42 .tree summary::marker,
43 .tree summary::-webkit-details-marker{
44 display : none;
45 }
46
47 .tree summary:focus{
48 outline : none;
49 }
50
51 .tree summary:focus-visible{
52 outline : 1px dotted #000;
53 }
54
55 .tree li::after,
56 .tree summary::before{
57 content : '';
58 display : block;
59 position : absolute;
60 top : calc(var(--spacing) / 2 - var(--radius));
61 left : calc(var(--spacing) - var(--radius) - 1px);
62 width : calc(2 * var(--radius));
63 height : calc(2 * var(--radius));
64 border-radius : 50%;
65 background : #ddd;
66 }
67
68 .tree summary::before{
69 content : '+';
70 z-index : 1;
71 background : #696;
72 color : #fff;
73 line-height : calc(2 * var(--radius) - 2px);
74 text-align : center;
75 }
76
77 .tree details[open] > summary::before{
78 content : '-';
79 }
Free content from Kate Rose Morley