Table of contents
PortedLinks to the sections of a page, with a bar on the current one. It only draws the list: the app sets current on an item, or lets useTableOfContentsActiveId follow the scroll. Its navigation landmark is named "Table of contents"; set aria-label for another name.
import { TableOfContents, TableOfContentsItem, TableOfContentsList, TableOfContentsTitle } from "@etched/monet/table-of-contents";
const headings = ["Summary", "Experience", "Interviews", "References", "Offer"];
export function Preview() {
return (
<div style={{ inlineSize: "12rem" }}>
<TableOfContents aria-label="Profile sections">
<TableOfContentsTitle>On this page</TableOfContentsTitle>
<TableOfContentsList>
{headings.map((heading) => (
<TableOfContentsItem key={heading} href={`#toc-preview-${heading.toLowerCase()}`} current={heading === "Interviews"}>
{heading}
</TableOfContentsItem>
))}
</TableOfContentsList>
</TableOfContents>
</div>
);
}Installation
import { TableOfContents, TableOfContentsGroup, TableOfContentsItem, TableOfContentsList, TableOfContentsTitle, useTableOfContentsActiveId } from "@etched/monet/table-of-contents";Examples
Scroll tracking
useTableOfContentsActiveId marks the first heading in view as you scroll the box. Here each item is a button that scrolls the box, and selectSection keeps the pressed item current until the scroll stops, also for the short last section.
Summary
Staff ASIC design engineer with nine years on timing closure and power grids, most recently on 5 nm inference parts.
Experience
Tenstorrent, 2021 to now: led physical design for two tape-outs. Apple, 2017 to 2021: power grid and IR drop on the A15 and A16.
Interviews
Recruiter screen on Oct 2, technical interview on Oct 9, and a four-person onsite on Oct 16. Three of four have sent feedback.
References
Sam Lee (manager at Groq) has replied. Kai Ito (peer at Cerebras) is scheduled for Thursday.
Offer
Waiting for the last interview feedback before the hiring committee meets.
import { useState } from "react";
import { Text } from "@etched/monet/text";
import { TableOfContents, TableOfContentsItem, TableOfContentsList, TableOfContentsTitle, useTableOfContentsActiveId } from "@etched/monet/table-of-contents";
const sections = [
{ id: "toc-summary", title: "Summary", text: "Staff ASIC design engineer with nine years on timing closure and power grids, most recently on 5 nm inference parts." },
{ id: "toc-experience", title: "Experience", text: "Tenstorrent, 2021 to now: led physical design for two tape-outs. Apple, 2017 to 2021: power grid and IR drop on the A15 and A16." },
{ id: "toc-interviews", title: "Interviews", text: "Recruiter screen on Oct 2, technical interview on Oct 9, and a four-person onsite on Oct 16. Three of four have sent feedback." },
{ id: "toc-references", title: "References", text: "Sam Lee (manager at Groq) has replied. Kai Ito (peer at Cerebras) is scheduled for Thursday." },
{ id: "toc-offer", title: "Offer", text: "Waiting for the last interview feedback before the hiring committee meets." },
];
export function ScrollTracking() {
const [root, setRoot] = useState<HTMLDivElement | null>(null);
const { activeId, selectSection } = useTableOfContentsActiveId({ ids: sections.map((s) => s.id), root, trackHash: false });
return (
<div style={{ display: "flex", gap: "var(--et-space-6)", maxInlineSize: "40rem" }}>
<TableOfContents aria-label="Candidate profile" style={{ flex: "none", inlineSize: "10rem" }}>
<TableOfContentsTitle>On this page</TableOfContentsTitle>
<TableOfContentsList>
{sections.map((section) => (
<TableOfContentsItem
key={section.id}
render={<button type="button" />}
current={activeId === section.id}
onClick={() => {
selectSection(section.id);
root?.querySelector(`#${section.id}`)?.scrollIntoView({ behavior: "smooth", block: "start" });
}}
>
{section.title}
</TableOfContentsItem>
))}
</TableOfContentsList>
</TableOfContents>
<div
ref={setRoot}
tabIndex={0}
aria-label="Profile"
role="region"
style={{ flex: 1, blockSize: "14rem", overflowY: "auto", padding: "var(--et-space-4)", scrollPaddingBlockStart: "var(--et-space-4)", borderRadius: "var(--et-card-radius)", boxShadow: "0 0 0 1px var(--et-line)" }}
>
{sections.map((section) => (
<div key={section.id} style={{ marginBlockEnd: "var(--et-space-8)" }}>
<Text as="h4" variant="heading" id={section.id}>
{section.title}
</Text>
<Text as="p" variant="secondary">
{section.text}
</Text>
</div>
))}
<div style={{ blockSize: "8rem" }} />
</div>
</div>
);
}Groups
A group's label is a link with href, another element with render (here a button, such as one that scrolls a box), or a plain label without either. Its items are indented under it.
import { TableOfContents, TableOfContentsGroup, TableOfContentsItem, TableOfContentsList, TableOfContentsTitle } from "@etched/monet/table-of-contents";
export function Groups() {
return (
<div style={{ inlineSize: "14rem" }}>
<TableOfContents aria-label="Settings sections">
<TableOfContentsTitle>Settings</TableOfContentsTitle>
<TableOfContentsList>
<TableOfContentsItem href="#toc-groups" current>
Overview
</TableOfContentsItem>
<TableOfContentsGroup label="Pipeline" href="#toc-groups">
<TableOfContentsItem href="#toc-groups">Stages</TableOfContentsItem>
<TableOfContentsItem href="#toc-groups">Scorecards</TableOfContentsItem>
</TableOfContentsGroup>
<TableOfContentsGroup label="Interviews" render={<button type="button" />}>
<TableOfContentsItem href="#toc-groups">Loops</TableOfContentsItem>
</TableOfContentsGroup>
<TableOfContentsGroup label="Notifications">
<TableOfContentsItem href="#toc-groups">Email digests</TableOfContentsItem>
<TableOfContentsItem href="#toc-groups">Slack alerts for new referrals and stage changes</TableOfContentsItem>
</TableOfContentsGroup>
</TableOfContentsList>
</TableOfContents>
</div>
);
}Accessibility
- Items are links, or the element in
render:Tabreaches them, andEnterfollows a link. - A
navlandmark named "Table of contents";aria-labelsets another name. Items are in a list; a group label withouthreforrenderis plain text. - The current item has
aria-current="true". - Mark one item
activeat a time, or letuseTableOfContentsActiveIdfollow the scroll. - With two on a page, give each its own
aria-label.
API reference
TableOfContentsGroup
| Prop | Type | Default | Description |
|---|---|---|---|
| label * | ReactNode | – | The text above the group's items. |
| href | string | – | Makes the label a link to its own section, in the look of an item. |
| render | ReactElement<{ className?: string }> | – | Another element for the label, as on TableOfContentsItem, such as a router link: render={<RouterLink to="/pipeline" />}. |
| current | boolean | false | The label's section is the current one. Only with href or render. |
| onClick | MouseEventHandler<HTMLAnchorElement> | – | Called when the label is pressed, not when a nested item is. Only with href or render. |
| children * | ReactNode | – | TableOfContentsItems, indented under the label. |
TableOfContentsItem
| Prop | Type | Default | Description |
|---|---|---|---|
| current | boolean | false | The current section: the bar turns to ink and the text to strong, with aria-current. |
| render | ReactElement<{ className?: string }> | – | Another element for the item, such as a router link, render={<RouterLink to="/intro" />}, or a button that scrolls a box, render={<button type="button" />}. It gets the item's props and look. |