Navigation

TableOfContents

Links to a page's headings that mark the section being read as it scrolls, and scroll smoothly there.

#navigation#document#interactive

Demo

Preview

Install

Install: a few paragraphs of the guide go here, long enough that the article scrolls and the list beside it follows where you are.

More about install, with examples and notes.

On macOS

On macOS: a few paragraphs of the guide go here, long enough that the article scrolls and the list beside it follows where you are.

More about on macos, with examples and notes.

On Linux

On Linux: a few paragraphs of the guide go here, long enough that the article scrolls and the list beside it follows where you are.

More about on linux, with examples and notes.

Your first program

Your first program: a few paragraphs of the guide go here, long enough that the article scrolls and the list beside it follows where you are.

More about your first program, with examples and notes.

Routes and views

Routes and views: a few paragraphs of the guide go here, long enough that the article scrolls and the list beside it follows where you are.

More about routes and views, with examples and notes.

Deploy

Deploy: a few paragraphs of the guide go here, long enough that the article scrolls and the list beside it follows where you are.

More about deploy, with examples and notes.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/table_of_contents.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/table_of_contents.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/table_of_contents.qwe
let table_of_contents_demo_items = [    { id: "toc-demo-install", text: "Install", level: 2 },    { id: "toc-demo-macos", text: "On macOS", level: 3 },    { id: "toc-demo-linux", text: "On Linux", level: 3 },    { id: "toc-demo-first", text: "Your first program", level: 2 },    { id: "toc-demo-routes", text: "Routes and views", level: 2 },    { id: "toc-demo-deploy", text: "Deploy", level: 2 }]view TableOfContentsDemo() =    <div style="display: flex; flex-wrap: wrap; gap: 24px; align-items: flex-start">        <div style="flex: 1 1 180px; max-width: 240px">            <TableOfContents id="toc-demo-title" items={table_of_contents_demo_items} scroller="toc-demo-article"/>        </div>        <article id="toc-demo-article" tabindex="0" aria-label="Getting started"                 style="flex: 3 1 300px; height: 320px; overflow: auto; padding: 0 18px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius)">            {for it in table_of_contents_demo_items => [                it.level == 2 ? <h2 id={it.id} style="font-size: 18px; margin: 20px 0 8px">{it.text}</h2>                              : <h3 id={it.id} style="font-size: 15px; margin: 16px 0 6px">{it.text}</h3>,                <p style="margin: 0 0 12px">{it.text}: a few paragraphs of the guide go here, long enough that the                    article scrolls and the list beside it follows where you are.</p>,                <p style="margin: 0 0 12px; color: var(--ui-muted)">More about {lower(it.text)}, with examples and notes.</p>            ]}            <div style="height: 120px"></div>        </article>    </div>

Source

ui/table_of_contents.qwe
// TableOfContents: the list of a page's headings, as links to them, that marks// the section being read as the page scrolls (aria-current="location"). A click// scrolls smoothly to the heading (at once when the system asks for less motion),// moves the focus there and puts its #id in the address bar. Without JavaScript// the links still go to the headings.////   <TableOfContents sticky={true} items={[//       { id: "install", text: "Install", level: 2 },//       { id: "install-macos", text: "On macOS", level: 3 },//       { id: "usage", text: "Usage", level: 2 }//   ]}/>//// items: the headings, in the page's order: { id (the heading's id), text, level// (2 for an <h2>, ...) }; deeper levels are indented under the one before.// label: its title, and what screen readers call it ("On this page").// offset: how many pixels at the top of the window are covered (by a sticky// header): a heading counts as reached, and a click stops, that far down (16).// scroller: the id of a scrolling element the headings are in, to follow instead of// the window. sticky: it stays in view as the page scrolls (top: var(--ui-toc-top, 16px)).// id: the title's id ("ui-toc"); give each one on a page its own.import ui/theme.qwe use ui_clslet table_of_contents_css = css`.ui-toc { min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-toc-sticky { position: sticky; top: var(--ui-toc-top, 16px); max-height: calc(100vh - var(--ui-toc-top, 16px) * 2); overflow: auto; }.ui-toc-title { margin: 0 0 8px; color: var(--ui-muted); font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }.ui-toc ol { margin: 0; padding: 0; list-style: none; border-left: 1px solid var(--ui-border); }.ui-toc li { margin: 0; }.ui-toc a {  display: block; margin-left: -1px; padding: 5px 10px 5px calc(12px + var(--ui-toc-depth, 0) * 14px);  border-left: 2px solid transparent; border-radius: 0 var(--ui-radius-sm) var(--ui-radius-sm) 0;  color: var(--ui-muted); text-decoration: none; overflow-wrap: anywhere; transition: color .15s, border-color .15s;}.ui-toc a:hover { color: var(--ui-fg); }.ui-toc a:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-toc a[aria-current] { border-left-color: var(--ui-accent); color: var(--ui-accent); font-weight: 500; }.ui-toc-deep a { font-size: 13px; }@media (prefers-reduced-motion: reduce) { .ui-toc a { transition: none; } }`component TableOfContents(items: list = [], label: str = "On this page", offset: int = 16, scroller: str? = null,                          sticky: bool = false, id: str = "ui-toc", class: str? = null) {    style global table_of_contents_css    state active = len(items) > 0 ? items[0].id : null    state box = null    state going = false    let top_level = min(items | map((it) => it.level ?? 2)) ?? 2    // The last heading scrolled past `offset`, or the last of all at the very bottom.    let spy = (_e = null) => {        if !going && len(items) > 0 {            let top = box != null ? box.getBoundingClientRect().top : 0            let bottom = box != null ? box.scrollTop + box.clientHeight >= box.scrollHeight - 2                : js("scrollY") + js("innerHeight") >= js("document.documentElement.scrollHeight") - 2            let passed = items | filter((it) => {                let h = js("document.getElementById", it.id)                h != null && h.getBoundingClientRect().top - top <= offset + 8            })            active = bottom ? items[-1].id : len(passed) > 0 ? passed[-1].id : items[0].id        }    }    let done = (_e = null) => {        going = false        spy()    }    let jump = (e, to: str) => {        // From the link's own document, not `js(...)`: code after a `js(...)` can run once        // the browser has followed the link, too late to stop it.        let h = e.target.ownerDocument.getElementById(to)        if h != null {            e.preventDefault()            let still = js("matchMedia", "(prefers-reduced-motion: reduce)").matches            let top = box != null ? box.getBoundingClientRect().top : 0            let by = h.getBoundingClientRect().top - top - offset            let at = box != null ? box.scrollTop : js("scrollY")            active = to            going = by > 1 || by < -1            (box ?? js("window")).scrollTo({ top: at + by, behavior: still ? "instant" : "smooth" })            if !h.hasAttribute("tabindex") && h.tabIndex < 0 { h.tabIndex = -1 }            h.focus({ preventScroll: true })            js("history.replaceState", js("history.state"), "", `#${to}`)        }    }    <nav class={ui_cls(["ui-toc", sticky && "ui-toc-sticky", class])} aria-labelledby={id}         @mount={(el) => {             box = scroller != null ? js("document.getElementById", scroller) : null             let from = box ?? js("window")             spy()             from.addEventListener("scroll", spy, { passive: true })             from.addEventListener("scrollend", done)             () => {                 from.removeEventListener("scroll", spy)                 from.removeEventListener("scrollend", done)             }         }}>        <p.ui-toc-title id={id}>{label}</p>        <ol>            {for it in items => <li key={it.id} class={(it.level ?? 2) > top_level ? "ui-toc-deep" : null}>                <a href={`#${it.id}`} style={`--ui-toc-depth: ${(it.level ?? 2) - top_level}`}                   aria-current={it.id == active ? "location" : null}                   @click={(e) => { jump(e, it.id) }}>{it.text}</a>            </li>}        </ol>    </nav>}
Thread
56Useful? Like it, or add to its thread below.

Thread 0 posts

On TableOfContents: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.