Files
leecat.art-site/tree-style-table-of-contents/index.html
T

2264 lines
62 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<title>tree-style table of contents | hello hello</title>
<meta name="description" content="Lee Cattarin... on the internet!">
<link rel="alternate" href="/feed.xml" type="application/atom+xml" title="hello hello">
<meta property="og:title" content="tree-style table of contents | hello hello">
<meta property="og:type" content="website">
<meta property="og:description" content="Lee Cattarin... on the internet!">
<meta property="og:site_name" content="hello hello">
<meta property="og:url" content="https://leecat.art/tree-style-table-of-contents/">
<meta property="og:image" content="https://leecat.art/img/tree-styling-0.png">
<meta property="og:image:alt" content="screenshot of the table of contents for a blog post (this blog post, in fact). it&#39;s been styled so that the output resembles the hierarchical file system output from the shell program &#39;tree.&#39;">
<meta name="generator" content="Eleventy v3.1.2">
<meta itemprop="ai-content-declaration:version" content="1.0.0">
<meta itemprop="ai-content-declaration:level" content="none">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:ital,wght@0,200..800;1,200..800&family=Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<script src="https://kit.fontawesome.com/884dded219.js" crossorigin="anonymous"></script>
<style>#toc {
float: right;
margin: 1rem 0 1.5rem 1.5rem;
/* 23rem is the width of the words 'table of contents' in my header font */
/* yes, it's a bit hacky */
width: min(calc(100% - 1.5rem), 23rem);
}
@media (max-width: 850px) {
#toc {
float: unset;
margin-top: 2rem;
margin-left: .5rem;
width: calc(100% - .5rem);
}
}
#toc details {
border: thick solid var(--color-pink);
border-radius: 1rem;
margin-top: 1rem;
background-color: var(--color-bg);
}
#toc summary {
position: relative;
top: -1.5rem;
left: -.75rem;
width: fit-content;
background: var(--color-bg);
border-radius: .5rem;
cursor: pointer;
}
#toc summary:focus-visible {
outline: .15rem solid var(--color-teal);
}
#toc summary::marker {
font-size: 2.35rem;
}
#toc h2 {
margin-top: 0;
display: inline;
line-height: normal;
}
#toc nav {
position: relative;
top: -1.25rem;
}
/* my baby my child my `tree` list style */
#toc ol,
#toc li {
margin-block: 0;
list-style: none;
}
#toc ol {
margin-left: 0;
}
#toc li {
border-left: .095rem solid var(--color-pink);
margin-left: 1.5rem;
line-height: 1.15;
text-indent: 1.22rem hanging;
}
#toc li::before {
content: '├─';
color: var(--color-pink);
position: relative;
left: -.34rem;
}
#toc li:last-child {
border-left-color: transparent;
}
#toc li:last-child::before {
/* Technically, we could use this content for all of them, but doing the full
bar for the non-last-childs keeps the border a *smidge* more consistent for
different levels of magnification */
content: '└─';
}
/* underlines are unnecessary in this context */
#toc a {
text-decoration: none;
color: var(--color-blue);
font-weight: bold;
}
#toc a:focus-visible {
outline-color: var(--color-pink);
}
.post-metadata {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: baseline;
margin: 1.5rem 0 .5rem;
}
.post-metadata p {
font-size: .9rem;
margin: 0;
}
.post-tags {
display: flex;
flex-flow: row wrap;
gap: .5rem;
list-style: none;
margin: 0;
margin-left: auto;
}
.post-tags li {
margin: 0;
}
.post-tags li a {
text-decoration: none;
color: var(--color-teal);
padding: 0 .5rem;
border-radius: 1rem;
box-shadow: .15rem .15rem var(--color-shadow);
border: .08rem solid var(--color-teal);
line-height: 2;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .1s ease-in, left .1s ease-in;
}
.post-tags li a:focus-visible {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
@media (any-hover: hover) {
.post-tags li a:hover {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
}
@media (forced-colors: active) {
.post-tags li a:focus-visible {
outline-offset: .08rem;
outline: .08rem solid;
}
@media (any-hover: hover) {
.post-tags li a:hover {
outline-offset: .08rem;
outline: .08rem solid;
}
}
}
/* Click animation */
.post-tags li a:active {
top: .1rem;
left: .1rem;
box-shadow: .05rem .05rem var(--color-shadow);
}
#postlist,
#taglist {
list-style: none;
}
#postlist, .post,
#taglist, .tag {
margin: 0;
}
/* Odd-numbered posts & tag layout/coloration */
.post:nth-child(odd) .postlink,
.tag:nth-child(odd) .taglink {
grid-template-areas:
'img h2'
'img info'
'img .';
grid-template-columns: 45% auto;;
--color-primary: var(--color-teal);
--color-accent: var(--color-pink);
}
/* Even-numbered posts & tags layout/coloration */
.post:nth-child(even) .postlink,
.tag:nth-child(even) .taglink {
grid-template-areas:
'h2 img'
'info img'
'. img';
grid-template-columns: auto 45%;
--color-primary: var(--color-pink);
--color-accent: var(--color-teal);
}
/* Layout for all posts on mobile */
@media (max-width: 650px) {
.post:nth-child(n) .postlink,
.tag:nth-child(n) .taglink {
grid-template-areas:
'img'
'h2'
'info';
grid-template-columns: auto;
}
}
/* Link */
.postlink,
.taglink {
display: grid;
border: .25rem solid var(--color-primary);
border-radius: 1.25rem;
box-shadow: .35rem .35rem var(--color-shadow);
margin: 2rem 0;
text-decoration: none;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .05s ease-in, left .05s ease-in;
}
.postlink:focus-visible,
.taglink:focus-visible {
background-color: var(--color-primary);
outline: none;
}
@media (any-hover: hover) {
.postlink:hover,
.taglink:hover {
background-color: var(--color-primary);
}
}
/* Forced colors */
@media (forced-colors: active) {
.postlink:focus-visible,
.taglink:focus-visible {
outline-offset: .25rem;
outline: .25rem solid;
}
@media (any-hover: hover) {
.postlink:hover,
.taglink:hover {
outline-offset: .25rem;
outline: .25rem solid;
}
}
}
/* Click animation */
.postlink:active,
.taglink:active {
top: .2rem;
left: .2rem;
box-shadow: .15rem .15rem var(--color-shadow);
}
/* Post & tag elements */
.post h2, .post img,
.post ul, .post li,
.tag h2, .tag p,
.tag img {
margin: 0;
}
.post h2,
.tag h2 {
grid-area: h2;
padding: .25rem .5rem;
text-transform: uppercase;
font-family: var(--font-family);
font-size: 1.5rem;
color: var(--color-primary);
border-radius: 1rem 1rem 0 0;
border-bottom: .25rem solid var(--color-accent);
}
.post:nth-child(even) h2,
.tag:nth-child(even) h2 {
text-align: right;
}
.postlink:focus-visible h2,
.taglink:focus-visible h2 {
color: var(--color-bg);
border-color: var(--color-bg);
}
@media (any-hover: hover) {
.postlink:hover h2,
.taglink:hover h2 {
color: var(--color-bg);
border-color: var(--color-bg);
}
}
/* Images */
.post img,
.tag-imgs {
grid-area: img;
}
.blur {
filter: blur(.75rem);
clip-path: inset(0 round 1rem);
}
.tag-imgs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: .15rem;
}
.tag-imgs img {
aspect-ratio: 3 / 2;
object-fit: cover;
}
.missing-image {
width: 100%;
aspect-ratio: 3 / 2;
background-color: var(--color-bg-alt);
border-radius: 1rem;
}
.taglink:focus-visible .missing-image {
opacity: .7;
}
@media (any-hover: hover) {
.taglink:hover .missing-image {
opacity: .7;
}
}
/* Post tags */
.postlist-tags {
grid-area: info;
list-style: none;
display: flex;
flex-flow: row wrap;
gap: .5rem;
padding: .5rem;
}
.post:nth-child(odd) .postlist-tags {
justify-content: flex-end;
}
.postlist-tags li,
.tagcount {
background-color: var(--color-primary);
color: var(--color-bg);
padding: 0 .5rem;
border-radius: 1rem;
}
.postlink:focus-visible .postlist-tags li,
.taglink:focus-visible .tagcount {
background-color: var(--color-bg);
color: var(--color-primary);
}
@media (any-hover: hover) {
.postlink:hover .postlist-tags li,
.taglink:hover .tagcount {
background-color: var(--color-bg);
color: var(--color-primary);
}
}
/* Tag count */
.tag p {
grid-area: info;
padding: .5rem;
}
.tag:nth-child(odd) p {
text-align: right;
}
img[tabindex="0"] {
cursor: pointer;
}
img[tabindex="0"]:focus-visible {
outline: .15rem solid var(--color-teal);
}
dialog {
margin: auto;
flex-flow: column;
background: transparent;
border: none;
position: absolute;
}
dialog[open] {
display: flex;
}
dialog::backdrop {
background-color: rgba(from var(--color-bg) r g b / .8);
backdrop-filter: blur(4px);
}
body:has(dialog[open]) {
overflow: hidden;
}
.close-dialog {
background-color: rgba(from var(--color-teal) r g b / .35);
font-size: 1.5rem;
padding: 0 .5rem .15rem;
border-radius: 1rem;
color: var(--color-teal);
box-shadow: .15rem .15rem var(--color-shadow);
border: .08rem solid var(--color-teal);
align-self: flex-end;
justify-self: flex-start;
margin: 0 .1rem;
/* Click animation handling */
position: relative;
top: 1rem;
left: -.15rem;
transition: top .1s ease-in, left .1s ease-in;
}
.close-dialog:focus-visible {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
@media (any-hover: hover) {
.close-dialog:hover {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
}
@media (forced-colors: active) {
.close-dialog:focus-visible {
outline-offset: .08rem;
outline: .08rem solid;
}
@media (any-hover: hover) {
.close-dialog:hover {
outline-offset: .08rem;
outline: .08rem solid;
}
}
}
/* Click animation */
.close-dialog:active {
top: 1.1rem;
left: -.05rem;
box-shadow: .05rem .05rem var(--color-shadow);
}
.modal-img {
max-height: calc(100vh - 2rem);
object-fit: contain;
width: auto;
max-width: 90%;
margin: 0 auto;
}
@font-face {
font-family: "Peritel";
src:
url("/fonts/Peritel/Peritel-Regular.woff2") format("woff2"),
url("/fonts/Peritel/Peritel-Regular.otf") format("opentype");
}
:root {
color-scheme: light dark;
--font-family: 'Atkinson Hyperlegible Next', sans-serif;
/* if you change the header font, go to post-toc.css and fuck about with the width
of the table of contents */
--font-family-headers: 'Peritel', 'Atkinson Hyperlegible Next', sans-serif;
--font-family-code: 'Atkinson Hyperlegible Mono', monospace;
--color-dark: #2e303e;
--color-dark-alt: #3c3f52;
--color-light: #ebeeef;
--color-light-alt: #dbe1e3;
--color-teal-dark: #18737b;
--color-teal-light: #25b0bc;
--color-pink-dark: #94195d;
--color-pink-light: #ee9fcb;
--color-shadow: rgba(2, 10, 40, .25);
/* Used for syntax highlighting */
--color-red-light: #f195aa;
--color-orange-light: #fab795;
--color-yellow-light: #fbe6bc;
--color-green-light: #29d398;
--color-blue-light: #26bbd9;
--color-purple-light: #ddaeea;
--color-grey-light: #b9c3c6;
--color-red-dark: #991433;
--color-orange-dark: #883206;
--color-yellow-dark: #6a4906;
--color-green-dark: #125940;
--color-blue-dark: #125663;
--color-purple-dark: #722999;
--color-grey-dark: #4a4b64;
--color-text: light-dark(var(--color-dark), var(--color-light));
--color-bg: light-dark(var(--color-light), var(--color-dark));
--color-text-alt: light-dark(var(--color-dark-alt), var(--color-light-alt));
--color-bg-alt: light-dark(var(--color-light-alt), var(--color-dark-alt));
--color-teal: light-dark(var(--color-teal-dark), var(--color-teal-light));
--color-pink: light-dark(var(--color-pink-dark), var(--color-pink-light));
--color-red: light-dark(var(--color-red-dark), var(--color-red-light));
--color-orange: light-dark(var(--color-orange-dark), var(--color-orange-light));
--color-yellow: light-dark(var(--color-yellow-dark), var(--color-yellow-light));
--color-green: light-dark(var(--color-green-dark), var(--color-green-light));
--color-blue: light-dark(var(--color-blue-dark), var(--color-blue-light));
--color-purple: light-dark(var(--color-purple-dark), var(--color-purple-light));
--color-grey: light-dark(var(--color-grey-dark), var(--color-grey-light));
}
:root:has(#theme-light:checked) {
color-scheme: light;
}
:root:has(#theme-dark:checked) {
color-scheme: dark;
}
/* Base */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family);
color: var(--color-text);
background-color: var(--color-bg);
}
main {
width: 60vw;
max-width: 1000px;
margin: 0 auto;
scroll-margin-top: 7rem;
}
@media (max-width: 1050px) {
main {
width: 75vw;
}
}
@media (max-width: 650px) {
main {
width: 92vw;
}
}
/* Headers */
h1, h2, h3, h4, h5, h6 {
line-height: 1.25;
color: var(--color-teal);
font-family: var(--font-family-headers);
letter-spacing: 0.1rem;
}
h1 {
margin-top: 3rem;
font-size: 3.7rem;
text-align: center;
}
h2, h3, h4, h5, h6 {
scroll-margin-top: 5rem;
}
h2 {
margin-top: 2rem;
font-size: 2.35rem;
}
h3 {
margin-top: 1.5rem;
font-size: 1.7rem;
}
@media (max-width: 650px) {
h1 { font-size: 2.95rem; }
h2 { font-size: 1.9rem; }
h3 { font-size: 1.4rem; }
}
h4, h5, h6 {
margin-top: 1rem;
font-size: 1.25rem;
}
/* Images */
img {
display: block;
max-width: 100%;
height: auto;
border-radius: 1rem;
margin: 0 auto;
}
/* Paragraphs */
p {
margin: 1.25rem 0;
line-height: 1.4;
}
strong,
b {
font-weight: 900;
}
/* Links */
a {
color: var(--color-text);
border-radius: .25rem;
text-decoration: underline;
text-decoration-style: solid;
text-decoration-thickness: .2em;
text-decoration-color: var(--color-teal);
transition: text-decoration-thickness .5s;
}
a:focus-visible {
text-decoration: none;
outline: .15rem solid var(--color-teal);
}
@media (any-hover: hover) {
a:hover {
text-decoration-thickness: .4em;
}
}
a:active {
text-decoration-thickness: .4em;
}
/* Heading anchors */
a.ha,
span.ha-placeholder {
color: var(--color-pink);
font-size: .8em;
vertical-align: .1em;
}
span.ha-placeholder {
opacity: .55;
}
/* Lists */
::marker {
color: var(--color-pink);
}
ul, ol, dl, li {
margin-left: 1rem;
}
@media (max-width: 650px) {
ul, ol, dl {
margin-left: 0;
}
}
li {
line-height: 1.4;
margin-top: .65rem;
margin-bottom: .65rem;
}
li ul, li ol {
margin: .5rem 0;
margin-left: 1rem;
}
@media (max-width: 650px) {
li ul, li ol {
margin-left: 0;
}
}
dt {
font-weight: 900;
margin-top: .5rem;
}
dd {
margin-left: 2rem;
margin-bottom: .75rem;
}
/* Blockquotes */
blockquote {
margin: .5rem 0;
padding: 0 1rem;
border-radius: .25rem 1rem 1rem .25rem;
line-height: 1.25;
border-left: .5rem solid var(--color-pink);
}
blockquote,
blockquote p,
blockquote ol,
blockquote ul {
background-color: var(--color-bg-alt);
padding: .5rem;
}
blockquote p {
margin: 0;
}
/* Tables */
table {
width: 100%;
border-spacing: 0; /* border collapse doesn't play nice with radii */
border-radius: .3rem;
border: thin solid var(--color-pink);
margin: 1.25rem 0;
}
th {
color: var(--color-bg);
background-color: var(--color-pink);
}
th code {
color: var(--color-text); /* Yes, I actually do this somewhere */
}
th, td {
padding: .5rem;
text-align: left;
}
tr:nth-child(even) { background-color: var(--color-bg-alt); }
th:not(:first-child) { border-left: thin solid var(--color-bg); }
th:first-child { border-top-left-radius: .25rem; }
th:last-child { border-top-right-radius: .25rem; }
td:not(:first-child) { border-left: thin solid var(--color-pink); }
/* Times */
time {
color: var(--color-grey);
}
/* Horizontal rules */
hr {
border: .25rem solid var(--color-pink);
margin: 2rem 0;
}
/* Used on home, reference, gallery pages */
.centered {
text-align: center;
}
/* Currently only used for resume, but it's generalizable */
.upper {
text-transform: uppercase;
}
/* Header */
header {
background-color: var(--color-bg);
padding: .75rem 0;
border-bottom: thick solid var(--color-teal);
box-shadow: 0 .25rem .15rem var(--color-shadow);
display: grid;
grid-template-columns: auto;
}
/* Header links, pagination links */
header a,
.pagination a,
.webring ul a {
border-radius: 1rem;
border: .125rem solid var(--color-pink);
color: var(--color-pink);
text-decoration: none;
padding: 0 .25rem;
box-shadow: .15rem .15rem var(--color-shadow);
font-size: 1.2rem;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .05s ease-in, left .05s ease-in;
}
header a,
.pagination .older a,
.webring .prev a,
.webring .rand a {
padding-right: .35rem;
}
.pagination .newer a,
.webring .next a,
.webring .rand a {
padding-left: .35rem;
}
header a:focus-visible,
.pagination a:focus-visible,
.webring ul a:focus-visible {
color: var(--color-bg);
border-color: var(--color-pink);
background-color: var(--color-pink);
outline: none;
}
@media (any-hover: hover) {
header a:hover,
.pagination a:hover,
.webring ul a:hover {
color: var(--color-bg);
border-color: var(--color-pink);
background-color: var(--color-pink);
}
}
@media (forced-colors: active) {
header a:focus-visible,
.pagination a:focus-visible,
.webring ul a:focus-visible {
outline-offset: .125rem;
outline: .125rem solid;
}
@media (any-hover: hover) {
header a:hover,
.pagination a:hover,
.webring ul a:hover {
outline-offset: .125rem;
outline: .125rem solid;
}
}
}
/* Click animation */
header a:active,
.pagination a:active,
.webring ul a:active {
top: .1rem;
left: .1rem;
box-shadow: .05rem .05rem var(--color-shadow);
}
/* Current page */
header a[aria-current="page"] {
border-color: var(--color-teal);
color: var(--color-teal);
}
header a[aria-current="page"]:focus-visible {
color: var(--color-bg);
border-color: var(--color-teal);
background-color: var(--color-teal);
}
@media (any-hover: hover) {
header a[aria-current="page"]:hover {
color: var(--color-bg);
background-color: var(--color-teal);
border-color: var(--color-teal);
}
}
/* Header link icons, pagination icons */
header i,
.pagination i,
.webring ul i {
color: var(--color-teal);
}
header i,
.pagination .older i,
.webring .prev i,
.webring .rand i:nth-child(1) {
padding-left: .25rem;
}
.pagination .newer i,
.webring .next i,
.webring .rand i:nth-child(2) {
padding-right: .25rem;
}
header a[aria-current="page"] i {
color: var(--color-pink);
}
header a:focus-visible i,
a[aria-current="page"] a:focus-visible i,
.pagination a:focus-visible i,
.webring ul a:focus-visible i {
color: var(--color-bg);
}
@media (any-hover: hover) {
header a:hover i,
header a[aria-current="page"]:hover i,
.pagination a:hover i,
.webring ul a:hover i {
color: var(--color-bg);
}
}
/* Skip link */
#skip {
left: -999px;
position: absolute;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
z-index: -99;
}
#skip:focus-visible {
display: inline-block;
left: auto;
top: auto;
width: auto;
height: auto;
overflow: auto;
margin: -.125rem 10%;
z-index: 999;
}
/* Nav */
header ul {
display: flex;
list-style: none;
gap: 1rem;
justify-content: center;
}
header ul,
header li {
margin: 0;
}
@media (max-width: 650px) {
.menu-text {
display: none; /* Icons only on small screens */
}
header a {
padding: .15rem .5rem;
}
header i {
padding: 0;
}
}
/* Footer */
footer {
padding: 1rem 0;
font-size: .9rem;
border-top: thick solid var(--color-pink);
margin-top: 2rem;
}
footer ul {
display: flex;
list-style: none;
gap: .5rem;
justify-content: center;
margin: 0;
}
footer li {
margin: 0;
}
footer li:nth-child(2)::before,
footer li:nth-child(2)::after {
content: " ● " / "";
color: var(--color-teal);
}
@media (max-width: 650px) {
footer ul {
flex-flow: column;
text-align: center;
}
footer li:nth-child(2)::before,
footer li:nth-child(2)::after {
content: none;
}
}
footer a {
text-decoration-color: var(--color-pink);
}
footer a:focus-visible {
outline-color: var(--color-pink);
}
/* color picker */
#theme {
display: flex;
justify-content: center;
margin: .5rem auto 0;
padding: 0 .75rem .5rem;
width: fit-content;
border: .125rem solid var(--color-teal);
border-radius: 2rem;
}
#theme legend {
margin: 0 auto;
color: var(--color-teal);
}
footer label {
cursor: pointer;
border: .125rem solid var(--color-pink);
padding: 0 .25rem;
color: var(--color-pink);
background-color: var(--color-bg); /* this stops the box shadows from showing through */
box-shadow: .15em .15em var(--color-shadow);
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .05s ease-in, left .05s ease-in;
}
footer input:checked + label {
color: var(--color-blue);
border-color: var(--color-blue);
}
footer label i {
color: var(--color-teal);
}
footer input:checked + label i {
color: var(--color-pink);
}
@media (any-hover: hover) {
footer label:hover,
footer input:checked + label:hover,
footer label:hover i,
footer input:checked + label:hover i {
color: var(--color-bg);
}
footer label:hover {
background-color: var(--color-pink);
}
footer input:checked + label:hover {
background-color: var(--color-teal);
}
}
footer input:focus-visible + label,
footer input:focus-visible + label i {
color: var(--color-bg);
background-color: var(--color-pink);
border-color: var(--color-pink);
}
footer input:checked:focus-visible + label,
footer input:checked:focus-visible + label i {
color: var(--color-bg);
background-color: var(--color-teal);
border-color: var(--color-teal);
}
footer label:active {
top: .09em;
left: .09em;
box-shadow: .05em .05em var(--color-shadow);
}
label.theme-light {
border-top-left-radius: 1rem;
border-bottom-left-radius: 1rem;
border-right: none;
padding-left: .35rem;
}
label.theme-auto {
}
label.theme-dark {
border-top-right-radius: 1rem;
border-bottom-right-radius: 1rem;
border-left: none;
padding-right: .35rem;
}
footer input {
/* To allow screen reader to still access these. */
/* From https://lyra.horse/blog/2025/08/you-dont-need-js/ */
opacity: 0;
position: absolute;
pointer-events: none;
}
/* Pagination */
.pagination,
.pagination li {
margin: 0;
}
.pagination {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-areas: "older newer";
list-style: none;
margin-top: 3rem;
}
@media (max-width: 650px) {
.post-pagination {
grid-template-columns: 1fr;
grid-template-areas:
"older"
"newer";
gap: .75rem;
}
}
.pagination .older {
grid-area: older;
}
.pagination .newer {
grid-area: newer;
text-align: right;
}
/* webring navigation */
.webring {
margin-bottom: 3rem;
}
.webring ul {
display: flex;
flex-flow: row wrap;
list-style: none;
justify-content: center;
gap: .5rem;
}
.webring ul,
.webring li {
margin-left: 0;
}
/* Handle color schemes */
:root:has(#theme-light:checked) .dark {
display: none;
}
:root:has(#theme-dark:checked) .light {
display: none;
}
@media (prefers-color-scheme: light) {
:root:not(:has(#theme-dark:checked)) .dark {
display: none;
}
}
@media (prefers-color-scheme: dark) {
:root:not(:has(#theme-light:checked)) .light {
display: none;
}
}
.searchbox {
margin: 1rem auto 0;
}
pagefind-searchbox {
/* Shadows */
--pf-scroll-shadow: var(--color-shadow) !important;
--pf-shadow-sm: .15rem .15rem 0 var(--color-shadow) !important;
--pf-shadow-md: .25rem .25rem 0 var(--color-shadow) !important;
--pf-shadow-lg: 1rem 1rem 0 var(--color-shadow) !important;
/* Typography */
--pf-font: var(--font-family) !important;
/* Border */
--pf-border-radius: 1.5rem !important;
/* Focus */
--pf-outline-width: .125rem !important;
--pf-outline-offset: 0 !important;
/* Width */
--pf-searchbox-max-width: min(550px, 92vw) !important;
}
.light pagefind-searchbox {
--pf-text: var(--color-dark) !important;
--pf-text-secondary: var(--color-dark-alt) !important;
--pf-text-muted: var(--color-dark-alt) !important;
--pf-background: var(--color-light-alt) !important;
--pf-border: var(--color-pink-dark) !important;
--pf-border-focus: var(--color-pink-dark) !important;
--pf-skeleton: var(--color-light) !important;
--pf-skeleton-shine: var(--color-dark) !important;
--pf-hover: var(--color-light) !important;
--pf-mark: var(--color-teal-dark) !important;
--pf-outline-focus: var(--color-teal-dark) !important;
/* Error */
--pf-error-bg: var(--color-light-alt) !important;
--pf-error-border: var(--color-red-dark) !important;
--pf-error-text: var(--color-red-dark) !important;
--pf-error-text-secondary: var(--color-red-dark) !important;
}
.dark pagefind-searchbox {
--pf-text: var(--color-light) !important;
--pf-text-secondary: var(--color-light-alt) !important;
--pf-text-muted: var(--color-light-alt) !important;
--pf-background: var(--color-dark-alt) !important;
--pf-border: var(--color-pink-light) !important;
--pf-border-focus: var(--color-pink-light) !important;
--pf-skeleton: var(--color-dark) !important;
--pf-skeleton-shine: var(--color-dark) !important;
--pf-hover: var(--color-dark) !important;
--pf-mark: var(--color-teal-light) !important;
--pf-outline-focus: var(--color-teal-light) !important;
/* Error */
--pf-error-bg: var(--color-dark-alt) !important;
--pf-error-border: var(--color-red-light) !important;
--pf-error-text: var(--color-red-light) !important;
--pf-error-text-secondary: var(--color-red-light) !important;
}
pagefind-searchbox input {
border-width: .125rem !important;
}
pagefind-searchbox input:not(:focus-visible) {
box-shadow: var(--pf-shadow-md) !important;
}
@media (max-width: 650px) {
.pf-trigger-shortcut {
display: none !important;
}
}
.pf-searchbox-dropdown,
.pf-searchbox-footer {
border-width: .125rem !important;
}
.pf-error {
margin-top: .5rem !important;
}
/* Adapted from PrismJS 1.30.0 Tomorrow Night theme
https://prismjs.com/download
*/
code,
pre,
code[class*=language-],
pre[class*=language-] {
font-family: var(--font-family-code);
background-color: var(--color-bg-alt);
font-size: .9em;
text-shadow: 0 1px var(--color-shadow);
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre,
pre[class*=language-] {
margin: 1rem 0;
padding: 1rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
border-radius: .5rem;
overflow: auto;
}
:not(pre)>code,
:not(pre)>code[class*=language-] {
padding: .2rem;
border-radius: .25rem;
white-space: normal;
}
/* Selected text */
code ::-moz-selection,
code::-moz-selection,
pre ::-moz-selection,
pre::-moz-selection,
code[class*=language-] ::-moz-selection,
code[class*=language-]::-moz-selection,
pre[class*=language-] ::-moz-selection,
pre[class*=language-]::-moz-selection,
code ::selection,
code::selection,
pre ::selection,
pre::selection,
code[class*=language-] ::selection,
code[class*=language-]::selection,
pre[class*=language-] ::selection,
pre[class*=language-]::selection {
text-shadow: none;
background-color: var(--color-bg);
}
/* Syntax highlighting */
.token.namespace {
opacity: .7;
}
.token.bold,
.token.important {
font-weight:700
}
.token.italic {
font-style:italic
}
.token.block-comment,
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog,
.token.punctuation {
color: var(--color-grey);
}
.token.attr-name,
.token.deleted,
.token.namespace,
.token.tag {
color: var(--color-red);
}
.token.boolean,
.token.function,
.token.number {
color: var(--color-orange);
}
.token.class-name,
.token.constant,
.token.property,
.token.symbol {
color: var(--color-yellow);
}
.token.attr-value,
.token.char,
.token.regex,
.token.string,
.token.variable,
.token.inserted {
color: var(--color-green);
}
.token.entity,
.token.operator,
.token.url,
.token.function-name {
color:var(--color-blue);
}
.token.atrule,
.token.builtin,
.token.important,
.token.keyword,
.token.selector {
color: var(--color-purple);
}
@media print {
/* Nav elements */
header,
footer,
nav {
display: none !important;
}
/* Base */
body {
background-color: #fff;
color: #000;
}
main {
width: 95vw;
}
h1,h2,h3,h4,h5,h6 {
color: #000;
}
/* Links */
/* Hover is not really necessary, but it's annoying when testing otherwise */
a,
a:hover {
text-decoration-style: dotted;
text-decoration-thickness: .1rem;
text-decoration-color: #000;
}
a::after{
content: " (" attr(href) ")";
}
/* Code */
code,
pre,
code[class*=language-],
pre[class*=language-] {
text-shadow: none;
background-color: var(--color-light);
color: #000 !important;
}
.token.namespace {
opacity: 1;
}
.token.block-comment,
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog,
.token.punctuation,
.token.attr-name,
.token.deleted,
.token.namespace,
.token.tag,
.token.boolean,
.token.function,
.token.number,
.token.class-name,
.token.constant,
.token.property,
.token.symbol,
.token.attr-value,
.token.char,
.token.regex,
.token.string,
.token.variable,
.token.inserted,
.token.entity,
.token.operator,
.token.url,
.token.function-name,
.token.atrule,
.token.builtin,
.token.important,
.token.keyword,
.token.selector {
color: #000;
}
}</style>
<script type="module">// Thank you to https://github.com/daviddarnes/heading-anchors
// Thank you to https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
let globalInstanceIndex = 0;
class HeadingAnchors extends HTMLElement {
static register(tagName = "heading-anchors", registry = window.customElements) {
if(registry && !registry.get(tagName)) {
registry.define(tagName, this);
}
}
static attributes = {
exclude: "data-ha-exclude",
prefix: "prefix",
content: "content",
}
static classes = {
anchor: "ha",
placeholder: "ha-placeholder",
srOnly: "ha-visualhide",
}
static defaultSelector = "h2,h3,h4,h5,h6";
static css = `
.${HeadingAnchors.classes.srOnly} {
clip: rect(0 0 0 0);
height: 1px;
overflow: hidden;
position: absolute;
width: 1px;
}
.${HeadingAnchors.classes.anchor} {
position: absolute;
left: var(--ha_offsetx);
top: var(--ha_offsety);
text-decoration: none;
opacity: 0;
}
.${HeadingAnchors.classes.placeholder} {
opacity: .3;
}
.${HeadingAnchors.classes.anchor}:is(:focus-within, :hover) {
opacity: 1;
}
.${HeadingAnchors.classes.anchor},
.${HeadingAnchors.classes.placeholder} {
display: inline-block;
padding: 0 .25em;
/* Disable selection of visually hidden label */
-webkit-user-select: none;
user-select: none;
}
@supports (anchor-name: none) {
.${HeadingAnchors.classes.anchor} {
position: absolute;
left: anchor(left);
top: anchor(top);
}
}`;
get supports() {
return "replaceSync" in CSSStyleSheet.prototype;
}
get supportsAnchorPosition() {
return CSS.supports("anchor-name: none");
}
constructor() {
super();
if(!this.supports) {
return;
}
let sheet = new CSSStyleSheet();
sheet.replaceSync(HeadingAnchors.css);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
this.headingStyles = {};
this.instanceIndex = globalInstanceIndex++;
}
connectedCallback() {
if (!this.supports) {
return;
}
this.headings.forEach((heading, index) => {
if(!heading.hasAttribute(HeadingAnchors.attributes.exclude)) {
let anchor = this.getAnchorElement(heading);
let placeholder = this.getPlaceholderElement();
// Prefers anchor position approach for better accessibility
// https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
if(this.supportsAnchorPosition) {
let anchorName = `--ha_${this.instanceIndex}_${index}`;
placeholder.style.setProperty("anchor-name", anchorName);
anchor.style.positionAnchor = anchorName;
}
heading.appendChild(placeholder);
heading.after(anchor);
}
});
}
// Polyfill-only
positionAnchorFromPlaceholder(placeholder) {
if(!placeholder) {
return;
}
let heading = placeholder.closest("h1,h2,h3,h4,h5,h6");
if(!heading.nextElementSibling) {
return;
}
// TODO next element could be more defensive
this.positionAnchor(heading.nextElementSibling);
}
// Polyfill-only
positionAnchor(anchor) {
if(!anchor || !anchor.previousElementSibling) {
return;
}
// TODO previous element could be more defensive
let heading = anchor.previousElementSibling;
this.setFontProp(heading, anchor);
if(this.supportsAnchorPosition) {
// quit early
return;
}
let placeholder = heading.querySelector(`.${HeadingAnchors.classes.placeholder}`);
if(placeholder) {
anchor.style.setProperty("--ha_offsetx", `${placeholder.offsetLeft}px`);
anchor.style.setProperty("--ha_offsety", `${placeholder.offsetTop}px`);
}
}
setFontProp(heading, anchor) {
let placeholder = heading.querySelector(`.${HeadingAnchors.classes.placeholder}`);
if(placeholder) {
let style = getComputedStyle(placeholder);
let props = ["font-weight", "font-size", "line-height", "font-family"];
let [weight, size, lh, family] = props.map(name => style.getPropertyValue(name));
anchor.style.setProperty("font", `${weight} ${size}/${lh} ${family}`);
let vars = style.getPropertyValue("font-variation-settings");
if(vars) {
anchor.style.setProperty("font-variation-settings", vars);
}
}
}
/* no longer used, using aria-labelledby instead - see getAnchorElement
getAccessibleTextPrefix() {
// Useful for i18n
return this.getAttribute(HeadingAnchors.attributes.prefix) || "Jump to section titled";
}
*/
getContent() {
if(this.hasAttribute(HeadingAnchors.attributes.content)) {
return this.getAttribute(HeadingAnchors.attributes.content);
}
return "#";
}
// Placeholder nests inside of heading
getPlaceholderElement() {
let ph = document.createElement("span");
ph.setAttribute("aria-hidden", true);
ph.classList.add(HeadingAnchors.classes.placeholder);
let content = this.getContent();
if(content) {
ph.innerHTML = content; // CHANGED HERE
}
ph.addEventListener("mouseover", (e) => {
let placeholder = e.target.closest(`.${HeadingAnchors.classes.placeholder}`);
if(placeholder) {
this.positionAnchorFromPlaceholder(placeholder);
}
});
return ph;
}
getAnchorElement(heading) {
let anchor = document.createElement("a");
anchor.href = `#${heading.id}`;
anchor.classList.add(HeadingAnchors.classes.anchor);
let content = this.getContent();
anchor.innerHTML = `<span aria-labelledby=${heading.id}>${content}</span>`; // CHANGED HERE
anchor.addEventListener("focus", e => {
let anchor = e.target.closest(`.${HeadingAnchors.classes.anchor}`);
if(anchor) {
this.positionAnchor(anchor);
}
});
anchor.addEventListener("mouseover", (e) => {
// when CSS anchor positioning is supported, this is only used to set the font
let anchor = e.target.closest(`.${HeadingAnchors.classes.anchor}`);
this.positionAnchor(anchor);
});
return anchor;
}
get headings() {
return this.querySelectorAll(this.selector.split(",").map(entry => `${entry.trim()}[id]`));
}
get selector() {
return this.getAttribute("selector") || HeadingAnchors.defaultSelector;
}
}
HeadingAnchors.register();
export { HeadingAnchors }
/* don't even bother on mobile */
if (window.innerWidth > 650) {
const dialog = document.querySelector("dialog");
const closeButton = document.querySelector(".close-dialog");
const hero = document.querySelector(".hero");
hero.tabIndex = 0;
hero.addEventListener("click", (e) => dialog.showModal());
hero.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
dialog.showModal();
}
});
closeButton.addEventListener("click", (e) => dialog.close());
closeButton.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
dialog.close();
}
});
}
const THEME = "theme";
const saveTheme = (themeValue) => {
localStorage.setItem(THEME, themeValue);
};
const themeLight = document.getElementById("theme-light");
themeLight.addEventListener("input", (e) => {
saveTheme(themeLight.value);
});
const themeAuto = document.getElementById("theme-auto");
themeAuto.addEventListener("input", (e) => {
saveTheme(themeAuto.auto);
});
const themeDark = document.getElementById("theme-dark");
themeDark.addEventListener("input", (e) => {
saveTheme(themeDark.value);
});
var currentThemeValue = localStorage.getItem(THEME);
switch (currentThemeValue) {
case "light":
themeLight.checked = true;
themeAuto.checked = false;
break;
case "dark":
themeDark.checked = true;
themeAuto.checked = false;
break;
case "auto":
default:
themeAuto.checked = true;
}</script>
<link href="/pagefind/pagefind-component-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-component-ui.js" type="module"></script>
</head>
<body>
<header>
<a href="#main" id="skip" title="skip to main content">
<i class="fa-solid fa-forward" aria-hidden="true"></i> skip
</a>
<nav aria-label="main navigation">
<ul>
<li>
<a href="/reference/" title="read reference posts">
<i class="fa-regular fa-folder-open" aria-hidden="true"></i>
<span class="menu-text">reference</span>
</a>
</li>
<li>
<a href="/gallery/" title="view the gallery">
<i class="fa-regular fa-images" aria-hidden="true"></i>
<span class="menu-text">gallery</span>
</a>
</li>
<li>
<a href="/" title="home">
<i class="fa fa-solid fa-crow" aria-hidden="true"></i>
<span class="menu-text">home</span>
</a>
</li>
<li>
<a href="/about/" title="about Lee">
<i class="fa-regular fa-user" aria-hidden="true"></i>
<span class="menu-text">about</span>
</a>
</li>
<li>
<a href="/contact/" title="contact Lee">
<i class="fa-solid fa-envelope-open-text" aria-hidden="true"></i>
<span class="menu-text">contact</span>
</a>
</li>
</ul>
</nav>
<div class="dark searchbox" data-pf-theme="dark">
<pagefind-searchbox></pagefind-searchbox>
</div>
<div class="light searchbox">
<pagefind-searchbox></pagefind-searchbox>
</div>
</header>
<main id="main">
<heading-anchors content="<i class='fa-solid fa-anchor'></i>">
<article data-pagefind-body="">
<h1>tree-style table of contents</h1>
<div class="post-metadata">
<p>
posted on
<time datetime="2026-09-21">September 21, 2026</time>
by Lee Cattarin
</p>
<ul class="post-tags">
<li>
<a href="/tags/software/">software</a>
</li>
</ul>
</div>
<dialog closedby="any" aria-label="image modal" tabindex="-1">
<button class="close-dialog" autofocus="" aria-label="close the image modal">&times;</button>
<img class="modal-img" src="/img/tree-styling-0.png" alt="screenshot of the table of contents for a blog post (this blog post, in fact). it&#39;s been styled so that the output resembles the hierarchical file system output from the shell program &#39;tree.&#39;" loading="lazy" decoding="async" width="621" height="450">
</dialog>
<img class="hero" src="/img/tree-styling-0.png" alt="screenshot of the table of contents for a blog post (this blog post, in fact). it&#39;s been styled so that the output resembles the hierarchical file system output from the shell program &#39;tree.&#39;" loading="lazy" decoding="async" width="621" height="450">
<aside id="toc">
<details>
<summary><h2>table of contents</h2></summary>
<nav class="toc">
<ol>
<li><a href="#tree">tree</a>
</li>
<li><a href="#html">HTML</a>
</li>
<li><a href="#css">CSS</a>
<ol>
<li><a href="#remove-styling-that-will-be-in-the-way">remove styling that will be in the way</a>
</li>
<li><a href="#start-the-structure-with-borders">start the structure with borders</a>
</li>
<li><a href="#li%3A%3Abefore">li::before</a>
</li>
<li><a href="#adjust-left-margins">adjust left margins</a>
</li>
<li><a href="#fix-those-pesky-last-children">fix those pesky last children</a>
</li>
<li><a href="#but-what-about-really%2C-really%2C-really%2C-really%2C-really-long-headers%3F">but what about really, really, really, really, REALLY long headers?</a>
</li>
</ol>
</li>
<li><a href="#summary">summary</a>
</li>
</ol>
</nav>
</details>
</aside>
<p>I have a shiny new Table of Contents (hereafter ToC) section for some of my posts (like this one)! And for fun and profit, it's styled like the output of <a href="https://en.wikipedia.org/wiki/Tree_(command)" target="_blank" rel="external"><code>tree</code></a>. I want to lay out the CSS I used for this. Fair warning, there's some hyper-specific border widths and positioning that <em>probably</em> should be a <code>calc()</code> function of some kind.</p>
<p>I drew inspiration from this <a href="https://stackoverflow.com/questions/14423671/best-method-for-displaying-a-tree-list-using-css-only-no-images-or-js-example" target="_blank" rel="external">StackOverflow issue regarding tree styling for lists</a>.</p>
<h2 id="tree"><code>tree</code></h2>
<p>Just for reference, here's some sample <code>tree</code> output:</p>
<pre class="language-txt"><code class="language-txt">.
├── footer.njk
├── head-content.njk
├── header.njk
├── layouts
│ ├── base.njk
│ ├── page-lists.njk
│ ├── page.njk
│ ├── post.njk
│ ├── post-with-toc.njk
│ └── tag-redirect.njk
├── pagination.njk
├── postlist.njk
└── webring.njk</code></pre>
<p>(You'll note that there are gaps between the verticals of each line. That's due to the line-height on the page, but in a standard terminal those vertical lines would be contiguous.)</p>
<h2 id="html">HTML</h2>
<p>Our ToC uses the following HTML structure:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>aside</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>toc<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>details</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>summary</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span><span class="token punctuation">></span></span>table of contents<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>summary</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>nav</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ol</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span><span class="token punctuation">></span></span>h2 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span><span class="token punctuation">></span></span>h2 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ol</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span><span class="token punctuation">></span></span>h3 2a<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span><span class="token punctuation">></span></span>h3 2b<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ol</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span><span class="token punctuation">></span></span>h2 3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span>
<span class="token comment">&lt;!-- etc --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ol</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>nav</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>details</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>aside</span><span class="token punctuation">></span></span></code></pre>
<p>We can ignore the <code>&lt;aside&gt;</code>, <code>&lt;details&gt;</code>, and <code>&lt;nav&gt;</code> components - we're going to focus on the <code>&lt;ol&gt;</code>s and <code>&lt;li&gt;</code>s.</p>
<h2 id="css">CSS</h2>
<p>Despite referencing a StackOverflow issue above, I never like to just copy-paste a chunk of StackOverflow code into place. I don't like extraneous CSS, and sometimes there's stuff mixed in there that just isn't necessary. Let's build from the ground up.</p>
<h3 id="remove-styling-that-will-be-in-the-way">remove styling that will be in the way</h3>
<p>By default, my lists have a lot of space around them for legibility when reading longer list items. We'll be accomplishing the vertical bars with borders here, so it's critical we not have any block-oriented spacing on <code>&lt;ol&gt;</code>s and <code>&lt;li&gt;</code>s. We're also going to take the numeric <code>list-style</code> off.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">#toc ol,
#toc li</span> <span class="token punctuation">{</span>
<span class="token property">margin-block</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
<span class="token property">list-style</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<h3 id="start-the-structure-with-borders">start the structure with borders</h3>
<p>The <code>tree</code>-like structure will be accomplished with both symbols (stolen directly from the tree output) and borders. Let's throw some borders on.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">#toc li</span> <span class="token punctuation">{</span>
<span class="token property">border-left</span><span class="token punctuation">:</span> .095rem solid <span class="token function">var</span><span class="token punctuation">(</span>--color-pink<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p><img src="/img/tree-styling-1.png" alt="Phase 1. There are thin pink vertical left-side borders for the list item elements, create long contiguous vertical lines. They are directly next to the list items with no spacing." loading="lazy" decoding="async" width="604" height="236"></p>
<p>Those are super off. First, they're tabbed too far in to the left; second, there's <em>no</em> space between the border and the list item. We're going to fix the first thing by removing left margins on the <code>&lt;ol&gt;</code>s. The second is where we pull in the most critical piece of infrastructure.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">#toc ol</span> <span class="token punctuation">{</span>
<span class="token property">margin-left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<h3 id="li%3A%3Abefore"><code>li::before</code></h3>
<p>We're going to use a character from the <code>tree</code> output and place it before our list items.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">#toc li::before</span> <span class="token punctuation">{</span>
<span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">'├─'</span><span class="token punctuation">;</span>
<span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-pink<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Notably, without additional positioning, the bars will be spaced away from the border. We want them to overlap perfectly.</p>
<p><img src="/img/tree-styling-2.png" alt="Phase 2, shown with the new characters before any positioning adjustment. There are tree line leaders before each list item, but they are a few pixels off from overlapping with the border." loading="lazy" decoding="async" width="598" height="224"></p>
<pre class="language-css"><code class="language-css"><span class="token selector">#toc li::before</span> <span class="token punctuation">{</span>
<span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span>
<span class="token property">left</span><span class="token punctuation">:</span> -.34rem<span class="token punctuation">;</span> <span class="token comment">/* I fiddled a lot and this is where I landed */</span>
<span class="token punctuation">}</span></code></pre>
<p><img src="/img/tree-styling-3.png" alt="Phase 3. The tree line leaders now perfectly overlap the border and it looks like a contiguous vertical line with small tabs inwards." loading="lazy" decoding="async" width="607" height="229"></p>
<h3 id="adjust-left-margins">adjust left margins</h3>
<p>Right now, the vertical lines don't nest nicely underneath the level above. Let's change our margins. I also adjusted the line height very slightly, because I noticed on some levels of magnification that the tree line leader characters weren't actually meeting.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">#toc li</span> <span class="token punctuation">{</span>
<span class="token property">margin-left</span><span class="token punctuation">:</span> 1.5rem<span class="token punctuation">;</span>
<span class="token property">line-height</span><span class="token punctuation">:</span> 1.15<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p><img src="/img/tree-styling-4.png" alt="Phase 4. The vertical border comes down directly below the first character of the parent level." loading="lazy" decoding="async" width="604" height="193"></p>
<h3 id="fix-those-pesky-last-children">fix those pesky last children</h3>
<p>You've probably noticed that the last children in the tree output had a bend rather than the T-shape we've been using. We'll want to remove the border from the last children and change the character we're using.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">#toc li:last-child</span> <span class="token punctuation">{</span>
<span class="token property">border-left-color</span><span class="token punctuation">:</span> transparent<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">#toc li:last-child::before</span> <span class="token punctuation">{</span>
<span class="token comment">/* Technically, we could use this content for all of them, but doing the full
bar for the non-last-childs keeps the border a *smidge* more consistent for
different levels of magnification */</span>
<span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">'└─'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>With that, it starts to look complete!</p>
<p><img src="/img/tree-styling-5.png" alt="Phase 5, almost done. The last children no longer have a border that continues onward as though there will be more children." loading="lazy" decoding="async" width="598" height="227"></p>
<h3 id="but-what-about-really%2C-really%2C-really%2C-really%2C-really-long-headers%3F">but what about really, really, really, really, REALLY long headers?</h3>
<p>What about line wrapping? It looks pretty bad right now.</p>
<p><img src="/img/tree-styling-6.png" alt="Table of contents now shown with the above header. When the line wraps, instead of lining up with the beginning of the first character on the previous line, it nests under the tree line leader and right in line with the vertical border." loading="lazy" decoding="async" width="609" height="405"></p>
<p>(And for the record, this happens for middle children too, it's not just a last child issue.)</p>
<p>We can fix this with a hanging indent.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">#toc li</span> <span class="token punctuation">{</span>
<span class="token property">text-indent</span><span class="token punctuation">:</span> 1.22rem hanging<span class="token punctuation">;</span> <span class="token comment">/* Again, I fiddled */</span>
<span class="token punctuation">}</span></code></pre>
<p><img src="/img/tree-styling-7.png" alt="Final version with the hanging indent implemented. Now the second line of text for this header lines up nicely with the start of the text above it." loading="lazy" decoding="async" width="608" height="408"></p>
<h2 id="summary">summary</h2>
<p>Here's the final collected CSS:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">#toc ol,
#toc li</span> <span class="token punctuation">{</span>
<span class="token property">margin-block</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
<span class="token property">list-style</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">#toc ol</span> <span class="token punctuation">{</span>
<span class="token property">margin-left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">#toc li</span> <span class="token punctuation">{</span>
<span class="token property">border-left</span><span class="token punctuation">:</span> .095rem solid <span class="token function">var</span><span class="token punctuation">(</span>--color-pink<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">margin-left</span><span class="token punctuation">:</span> 1.5rem<span class="token punctuation">;</span>
<span class="token property">line-height</span><span class="token punctuation">:</span> 1.15<span class="token punctuation">;</span>
<span class="token property">text-indent</span><span class="token punctuation">:</span> 1.22rem hanging<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">#toc li::before</span> <span class="token punctuation">{</span>
<span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">'├─'</span><span class="token punctuation">;</span>
<span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-pink<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span>
<span class="token property">left</span><span class="token punctuation">:</span> -.34rem<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">#toc li:last-child</span> <span class="token punctuation">{</span>
<span class="token property">border-left-color</span><span class="token punctuation">:</span> transparent<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">#toc li:last-child::before</span> <span class="token punctuation">{</span>
<span class="token comment">/* Technically, we could use this content for all of them, but doing the full
bar for the non-last-childs keeps the border a *smidge* more consistent for
different levels of magnification */</span>
<span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">'└─'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
</article>
<nav aria-label="pagination">
<ol class="pagination post-pagination">
<li class="older">
<a href="/houndstooth-dishtowels/">
<i class="fa-solid fa-hand-point-left" aria-hidden="true"></i> houndstooth dishtowels
</a>
</li>
</ol>
</nav>
<hr>
<section class="related-posts">
<h2 data-ha-exclude="">related posts</h2>
<ol id="postlist">
<li class="post">
<a class="postlink" href="/designing-a-bag/">
<h2 data-ha-exclude="">designing a bag </h2>
<ul class="postlist-tags">
<li>leather</li>
<li>software</li>
</ul>
<img src="/img/shoelace-bag.jpg" alt="a 3-image collage showcasing a leather crossbody bag. the leather body is brown and fairly simple. up the narrow sides, rope is laced through grommets in a style resembling a shoe lacing. the rope forms the handle and loops seamlessly through the other side of the bag, joining in one point in a figure-8 follow-through knot. At the bottom corners, there are small diagonal lines of stitching to give the bag a small lip around the base and ensure small objects don&#39;t slide out." loading="lazy" decoding="async" width="1000" height="1777">
</a>
</li>
<li class="post">
<a class="postlink" href="/intro-to-wireframing/">
<h2 data-ha-exclude="">intro to wireframing </h2>
<ul class="postlist-tags">
<li>software</li>
</ul>
<img src="/img/aggregator-wireframes.jpg" alt="a figma page with 4 major sections titled aggregator, aggregator mobile, aggregator color, and aggregator mobile color. each section has 7 pages in it - all sections pretty clearly have the same 7 pages, with the mobile sections shown on mobile screens and the color sections in a rainbow of pastels rather than grayscale." loading="lazy" decoding="async" width="1000" height="1042">
</a>
</li>
<li class="post">
<a class="postlink" href="/eleventy-lessons/">
<h2 data-ha-exclude="">eleventy lessons </h2>
<ul class="postlist-tags">
<li>software</li>
</ul>
<img src="/img/hellebore.jpg" alt="Image unrelated to post. Close up on a pale green hellebore flower." loading="lazy" decoding="async" width="1000" height="666">
</a>
</li>
</ol>
</section>
</heading-anchors>
</main>
<footer>
<ul>
<li>
<a href="/colophon/">
colophon
</a>
</li>
<li>
<a href="/" title="go home" aria-label="go home | hello hello from Lee Cattarin, copyright 2022-2026">
hello hello from Lee Cattarin, © 2022-2026</a>
</li>
<li>
<a href="https://heckin.technology/inherentlee/leecat.art" title="source code" aria-label="source code" target="_blank" rel="external">
src
</a>
</li>
</ul>
<fieldset id="theme">
<legend>color scheme</legend>
<input type="radio" id="theme-light" name="theme" value="light">
<label class="theme-light" for="theme-light">
<i class="fa-regular fa-sun" aria-hidden="true"></i>
light
</label>
<input type="radio" id="theme-auto" name="theme" value="auto" checked="">
<label class="theme-auto" for="theme-auto">
auto
</label>
<input type="radio" id="theme-dark" name="theme" value="dark">
<label class="theme-dark" for="theme-dark">
dark
<i class="fa-regular fa-moon" aria-hidden="true"></i>
</label>
</fieldset>
</footer>
<!-- This page `/tree-style-table-of-contents/` was built on 2026-09-21T19:19:24.068Z -->
</body>
</html>