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.
The HTML
We begin with the HTML for simple nested lists:
|
|
We then add a class to the outermost <ul>
element, and for each list item that contains a nested list, we put the contents of the list item inside <details>
and <summary>
elements, using the open
attribute to control which nested lists are initially expanded:
|
|
Without any styling, this HTML produces:
-
Giant planets
-
Gas giants
- Jupiter
- Saturn
-
Ice giants
- Uranus
- Neptune
-
The browser implements the <details>
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:
|
|
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:
|
|
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
- Jupiter
- Saturn
-
Ice giants
- Uranus
- 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:
|
|
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
- Jupiter
- Saturn
-
Ice giants
- Uranus
- Neptune
-
Horizontal lines
We use generated content to add the horizontal lines that join the vertical lines to the markers of each list item:
|
|
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
- Jupiter
- Saturn
-
Ice giants
- Uranus
- Neptune
-
Summaries
Next we remove the default styling from summaries:
|
|
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
- Jupiter
- Saturn
-
Ice giants
- Uranus
- Neptune
-
Markers
We use generated content again to create the markers:
|
|
Note that we generate markers both for <li>
elements (for list items that don’t contain nested lists) and for <summary
elements, allowing list items that contain nested lists to have different marker styling depending on whether the nested list is expanded or collapsed.
Lines 57 and 58 generate a block, and lines 59 to 61 center it over the corner where the horizontal and vertical lines meet. The top is positioned at the midpoint of the line of text, minus the radius. The left is positioned at the edge of the vertical line, minus the radius, minus the one pixel corresponding to half the line width.
Lines 62 and 63 set the size of the block, and lines 64 and 65 style it as a circle.
Applying this styling produces:
-
Giant planets
-
Gas giants
- Jupiter
- Saturn
-
Ice giants
- Uranus
- Neptune
-
Expand and collapse buttons
Finally, we add the expand and collapse buttons:
|
|
Line 69 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 70 and 74 show plus and minus signs in the buttons. You can download expand-collapse.svg (199 bytes) for use on your own site.
Applying this styling produces the finished tree view:
The finished code
Combining all of the above leads to the finished code:
|
|