Monet
@etched/monet 0.0.1

Table of contents

Ported

Links 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.

Kumo source
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: Tab reaches them, and Enter follows a link.
  • A nav landmark named "Table of contents"; aria-label sets another name. Items are in a list; a group label without href or render is plain text.
  • The current item has aria-current="true".
  • Mark one item active at a time, or let useTableOfContentsActiveId follow the scroll.
  • With two on a page, give each its own aria-label.

API reference

TableOfContentsGroup

PropTypeDefaultDescription
label *ReactNode–The text above the group's items.
hrefstring–Makes the label a link to its own section, in the look of an item.
renderReactElement<{ className?: string }>–Another element for the label, as on TableOfContentsItem, such as a router link: render={<RouterLink to="/pipeline" />}.
currentbooleanfalseThe label's section is the current one. Only with href or render.
onClickMouseEventHandler<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

PropTypeDefaultDescription
currentbooleanfalseThe current section: the bar turns to ink and the text to strong, with aria-current.
renderReactElement<{ 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.