דילוג לתוכן
uiify

Table of contents

ניווט קומפקטי בכותרות עם הגדלה בריחוף ומעקב אחר המיקום בגלילה.

דפדפנים
Chrome 123+Firefox 121+Safari 17.5+
שכבה
2
גודל חבילה דחוסה
4.14 kB
מגבלת פריסה כפויה
0 ms
מגבלת Axe
0
תואם SSR
כן
חוזה מפורסם
סביבת בדיקות

מגרש ניסויים

Overview

A table of contents gives readers a quick map of a long page. Follow a heading to jump directly to its section, or keep reading and let the current position update as you scroll.

Installation

Import the client entry to enable scroll tracking and pointer interaction. The native button and heading links are already usable before the page finishes loading.

Configuration

Match every line and link to a unique heading ID. Choose a scroll container and a content boundary when the article lives inside another page.

Examples

Hover a line to see its heading in the contents list. Click the line to jump there. On a touch screen, open the list and select the heading you want to read.

Accessibility

The contents are ordinary navigation links. Use the keyboard to open the list, move through its headings, and dismiss it with Escape. Scrolling never moves keyboard focus.

Styling

Adjust line length, spacing and the panel width with CSS variables. The current section uses bolder text; the heading under the pointer gets a separate hover background.

Conclusion

The same component works beside a document or inside a reader. Its compact rail stays available while the article scrolls, without taking a separate content column.

Introduction

A table of contents gives readers a map of a longer article. Open the contents or choose a bar to jump to a section. Scroll this article to follow your position in the rail.

Examples

Keep navigation beside the content it describes. Each of the four bars and its matching link points to a real section in this preview, so you can explore without leaving the card.

API

Give each section a unique ID and use that same ID for its link and indicator. Scope scroll tracking to the article’s container when the content sits inside another page.

Styling

Use CSS to adjust the rail, spacing and panel. The active bar follows the current section. Native heading links continue to work when JavaScript is unavailable.

import { TableOfContents } from "@gunkin/uiify/components/table-of-contents/client";

const sections = ["Overview", "Installation", "Configuration", "Examples", "Accessibility", "Styling", "Conclusion"];
export default function Example() {
  return (
    <div className="toc-reader" id="toc-reader" tabIndex={0} role="region" aria-label="Example article">
      <TableOfContents.Root
        className=""
        aria-label="Contents"
        scrollTarget="toc-reader"
        contentTarget="toc-article"
        offset={24}
      >
        <TableOfContents.Trigger className="" target="toc-panel" aria-label="Contents">
          <TableOfContents.Rail className="">
            {sections.map((label, index) => (
              <TableOfContents.Indicator className="" key={label} value={`toc-section-${index}`} />
            ))}
          </TableOfContents.Rail>
        </TableOfContents.Trigger>
        <TableOfContents.Content className="" id="toc-panel" side="right" align="center">
          <TableOfContents.List className="">
            {sections.map((label, index) => (
              <TableOfContents.Link className="" key={label} href={`#toc-section-${index}`}>
                {label}
              </TableOfContents.Link>
            ))}
          </TableOfContents.List>
        </TableOfContents.Content>
      </TableOfContents.Root>
      <article id="toc-article">
        {sections.map((label, index) => (
          <section id={`toc-section-${index}`} key={label}>
            <h2>{label}</h2>
            <p>Read this section, or choose another heading from the contents.</p>
          </section>
        ))}
      </article>
    </div>
  );
}
@layer components {
  [data-uiify-table-of-contents] {
    --uiify-toc-line-size: 12px;
    --uiify-toc-line-max-size: 48px;
    --uiify-toc-line-thickness: 2px;
    --uiify-toc-row-size: 20px;
    --uiify-toc-magnify-radius: 2.5;
    --uiify-toc-indent: 1rem;
    --uiify-toc-panel-size: 18rem;
    --uiify-toc-duration: 140ms;

    color: var(--foreground);
  }
  [data-uiify-table-of-contents] [data-part="trigger"] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-inline-size: var(--control-md);
    min-block-size: var(--control-md);
    padding: 0.75rem;
    border: 0;
    border-radius: var(--radius-md);
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: pointer;
  }
  [data-uiify-table-of-contents] [data-part="rail"] {
    display: grid;
    grid-auto-rows: var(--uiify-toc-row-size);
    align-items: center;
    inline-size: var(--uiify-toc-line-max-size);
    max-block-size: calc(100dvb - 8rem);
    overflow: auto;
  }
  [data-uiify-table-of-contents] [data-part="indicator"] {
    display: block;

    --uiify-toc-distance: max(
      calc(var(--uiify-toc-index, 0) - var(--uiify-toc-pointer, -1000)),
      calc(var(--uiify-toc-pointer, -1000) - var(--uiify-toc-index, 0))
    );
    --uiify-toc-proximity: clamp(
      0,
      calc(1 - var(--uiify-toc-distance) / max(0.001, var(--uiify-toc-magnify-radius))),
      1
    );

    inline-size: calc(
      var(--uiify-toc-line-size) + (var(--uiify-toc-line-max-size) - var(--uiify-toc-line-size)) *
        var(--uiify-toc-proximity)
    );
    transition:
      inline-size var(--uiify-toc-duration) ease,
      opacity var(--uiify-toc-duration) ease;
    block-size: var(--uiify-toc-line-thickness);
    border-radius: var(--radius-full);
    background: var(--muted-foreground);
    opacity: 0.5;
  }
  [data-uiify-table-of-contents] [data-part="indicator"][data-selected] {
    background: var(--foreground);
    opacity: 1;
  }
  [data-uiify-table-of-contents] [data-part="indicator"][data-highlighted]:not([data-selected]) {
    background: var(--primary);
    opacity: 1;
  }
  [data-uiify-table-of-contents] [data-part="content"] {
    --uiify-anchor-offset: 0;

    inline-size: min(var(--uiify-toc-panel-size), calc(100dvi - 2rem));
    max-block-size: calc(100dvb - 2rem);
    overflow: auto;
    overscroll-behavior: contain;
    padding: 0.5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--popover);
    color: var(--popover-foreground);
  }
  [data-uiify-table-of-contents] [data-part="list"] {
    display: grid;
  }
  [data-uiify-table-of-contents] [data-part="link"] {
    display: block;
    padding-block: 0.625rem;
    padding-inline: calc(0.75rem + max(0, var(--uiify-toc-level, 2) - 2) * var(--uiify-toc-indent))
      0.75rem;
    border-radius: var(--radius-md);
    color: var(--muted-foreground);
    text-decoration: none;
    overflow-wrap: anywhere;
    font-size: 0.875rem;
    line-height: 1.4;
  }
  [data-uiify-table-of-contents] [data-part="link"][data-selected] {
    color: var(--foreground);
    font-weight: var(--weight-semibold);
  }
  [data-uiify-table-of-contents] [data-part="link"]:is(:hover, [data-highlighted]) {
    background: var(--accent);
    color: var(--accent-foreground);
  }
  [data-uiify-table-of-contents] :is([data-part="trigger"], [data-part="link"]):focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
  }

  @media (prefers-reduced-motion: reduce) {
    [data-uiify-table-of-contents] [data-part="indicator"] {
      transition: none;
    }
  }
}
.toc-reader { display: grid; grid-template-columns: 3rem minmax(0, 1fr); align-items: start; block-size: 28rem; overflow: auto; }
.toc-reader > nav { grid-column: 1; grid-row: 1; position: sticky; inset-block-start: 50%; translate: 0 -50%; --uiify-toc-line-size: 6px; --uiify-toc-line-max-size: 24px; }
.toc-reader > article { grid-column: 2; grid-row: 1; min-inline-size: 0; }
.toc-reader section { min-block-size: 18rem; padding: 1rem; scroll-margin-block-start: 24px; }
import { TableOfContents } from "@gunkin/uiify/components/table-of-contents/client";

const sections = ["Overview", "Installation", "Configuration", "Examples", "Accessibility", "Styling", "Conclusion"];
export default function Example() {
  return (
    <div
      className="grid grid-cols-[3rem_minmax(0,1fr)] [block-size:28rem] overflow-auto [&>article]:[min-inline-size:0] [&_section]:[min-block-size:18rem] [&_section]:p-4"
      id="toc-reader"
      tabIndex={0}
      role="region"
      aria-label="Example article"
    >
      <TableOfContents.Root
        className="
          [--uiify-toc-line-size:12px] [--uiify-toc-line-max-size:48px] [--uiify-toc-line-thickness:2px]
          [--uiify-toc-row-size:20px] [--uiify-toc-magnify-radius:2.5] [--uiify-toc-indent:1rem]
          [--uiify-toc-panel-size:18rem] [--uiify-toc-duration:140ms] text-foreground
        "
        aria-label="Contents"
        scrollTarget="toc-reader"
        contentTarget="toc-article"
        offset={24}
      >
        <TableOfContents.Trigger
          className="
            inline-flex items-center justify-center [min-inline-size:var(--control-md)]
            [min-block-size:var(--control-md)] [padding:0.75rem] border-0 border-current border-none
            [border-radius:var(--radius-md)] bg-transparent text-inherit [font-style:inherit] [font-variant:inherit]
            [font-weight:inherit] [font-stretch:inherit] text-[length:inherit] leading-[inherit] [font-family:inherit]
            cursor-pointer focus-visible:[outline:2px_solid_var(--ring)] focus-visible:[outline-offset:2px]
          "
          target="toc-panel"
          aria-label="Contents"
        >
          <TableOfContents.Rail
            className="
              grid [grid-auto-rows:var(--uiify-toc-row-size)] items-center [inline-size:var(--uiify-toc-line-max-size)]
              [max-block-size:calc(100dvb_-_8rem)] overflow-auto
            "
          >
            {sections.map((label, index) => (
              <TableOfContents.Indicator
                className={
                  "block [--uiify-toc-distance:max(_calc(var(--uiify-toc-index,_0)_-_var(--uiify-toc-pointer,_-1000)" +
                  "),_calc(var(--uiify-toc-pointer,_-1000)_-_var(--uiify-toc-index,_0))_)] [--uiify-toc-proximity:cl" +
                  "amp(_0,_calc(1_-_var(--uiify-toc-distance)_/_max(0.001,_var(--uiify-toc-magnify-radius))),_1_)] [" +
                  "inline-size:calc(_var(--uiify-toc-line-size)_+_(var(--uiify-toc-line-max-size)_-_var(--uiify-toc-" +
                  "line-size))_*_var(--uiify-toc-proximity)_)] [transition:inline-size_var(--uiify-toc-duration)_eas" +
                  "e,_opacity_var(--uiify-toc-duration)_ease] [block-size:var(--uiify-toc-line-thickness)] rounded-[" +
                  "var(--radius-full)] bg-muted-foreground opacity-50 data-[selected]:bg-foreground data-[selected]:" +
                  "opacity-100 [&[data-highlighted]:not([data-selected])]:bg-primary [&[data-highlighted]:not([data-" +
                  "selected])]:opacity-100 motion-reduce:[transition:none]"
                }
                key={label}
                value={`toc-section-${index}`}
              />
            ))}
          </TableOfContents.Rail>
        </TableOfContents.Trigger>
        <TableOfContents.Content
          className="
            [--uiify-anchor-offset:0] [inline-size:min(var(--uiify-toc-panel-size),_calc(100dvi_-_2rem))]
            [max-block-size:calc(100dvb_-_2rem)] overflow-auto overscroll-contain [padding:0.5rem] [border-width:1px]
            border-solid border-border [border-radius:var(--radius-lg)] bg-popover text-popover-foreground
          "
          id="toc-panel"
          side="right"
          align="center"
        >
          <TableOfContents.List className="grid">
            {sections.map((label, index) => (
              <TableOfContents.Link
                className={
                  "block [padding-block:0.625rem] [padding-inline:calc(0.75rem_+_max(0,_var(--uiify-toc-level,_2)_-_" +
                  "2)_*_var(--uiify-toc-indent))_0.75rem] [border-radius:var(--radius-md)] text-muted-foreground no-" +
                  "underline [overflow-wrap:anywhere] [font-size:0.875rem] [line-height:1.4] data-[selected]:text-fo" +
                  "reground data-[selected]:font-[number:var(--weight-semibold)] hover:bg-accent hover:text-accent-f" +
                  "oreground data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground focus-visible:[o" +
                  "utline:2px_solid_var(--ring)] focus-visible:[outline-offset:2px]"
                }
                key={label}
                href={`#toc-section-${index}`}
              >
                {label}
              </TableOfContents.Link>
            ))}
          </TableOfContents.List>
        </TableOfContents.Content>
      </TableOfContents.Root>
      <article id="toc-article">
        {sections.map((label, index) => (
          <section id={`toc-section-${index}`} key={label}>
            <h2>{label}</h2>
            <p>Read this section, or choose another heading from the contents.</p>
          </section>
        ))}
      </article>
    </div>
  );
}

API

TableOfContents.Root (/client)

מאפייןטיפוסברירת מחדלתיאור
scrollSpybooleantrueמעקב אחר יעדים באותו עמוד לפי סדר המסמך לאחר ההידרציה.
scrollTargetstring—מזהה האלמנט הנגלל. יש להשמיט כדי לעקוב אחר החלון שאליו הוא שייך; מזהה שאינו נמצא אינו מפעיל חלופה.
contentTargetstring—מזהה אלמנט התוכן, המגביל את איתור היעדים וקובע את סוף המאמר.
offsetnumber0מספר הפיקסלים מתחת לתחילת אזור הגלילה המשמש לבחירת הכותרת הנוכחית.
valuestring | null—מזהה מבוקר של היעד הנוכחי. null מנקה את הבחירה.
defaultValuestring | nullnullמזהה התחלתי של היעד הנוכחי.
onValueChange(value: string | null) => void—מדווחת על שינויים במעקב הגלילה בלי להזיז את המיקוד.
openboolean—מצב מבוקר של החלונית הקופצת.
defaultOpenbooleanfalseמצב פתיחה התחלתי המוחל לאחר ההידרציה.
onOpenChange(open: boolean) => void—מדווחת על בקשות ריחוף, הפעלה וסגירה מובנית.
openOnHoverbooleantrueפתיחה בריחוף עכבר; במגע נעשה שימוש בכפתור המובנה.
openDelaynumber100השהיית פתיחה בריחוף עכבר באלפיות השנייה.
closeDelaynumber200פרק הזמן המותר למעבר בין כפתור הפתיחה לרשימה.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

TableOfContents.Trigger

מאפייןטיפוסברירת מחדלתיאור
target*string—מזהה Content ייחודי, המקושר באמצעות popovertarget מובנה.
aria-labelstring—שם נגיש הנדרש כשכפתור הפתיחה מכיל רק את המסילה הדקורטיבית.

TableOfContents.Indicator

מאפייןטיפוסברירת מחדלתיאור
value*string—מזהה יעד התואם למקטע של Link; מחוון אחד לכל מקטע.
level1 | 2 | 3 | 4 | 5 | 62עומק הכותרת הזמין דרך --uiify-toc-level.
selectedbooleanfalseבחירת רשומה בשרת; הלקוח גוזר את הבחירה מתוך Root.value.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

TableOfContents.Content

מאפייןטיפוסברירת מחדלתיאור
id*string—מזהה חלונית קופצת ייחודי התואם ל-Trigger.target.
side"top" | "right" | "bottom" | "left""left"צד העוגן המובנה ב-CSS; הפריסה מתהפכת בעת הצורך.
align"start" | "center" | "end""start"יישור לצד כפתור הפתיחה.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

TableOfContents.Link

TableOfContents.Rail / List

מאפייןטיפוסברירת מחדלתיאור
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

מקלדת

מקשפעולה
Enter / Spaceפתיחה או סגירה של חלונית תוכן העניינים המובנית באמצעות המפעיל שלה
Tab / Shift+Tabמעבר בין קישורי כותרות רגילים
Escapeסגירת החלונית הקופצת והחזרת המיקוד מהרשימה למפעיל שלה

נגישות

תנו תווית לאזור הניווט ולמפעיל שלו הכולל סמל בלבד. נקודת הכניסה לשרת תומכת בהפעלה ובסגירה מובנות של חלונית קופצת ובקישורי מקטעים לפני הידרציה; נקודת הכניסה /client מוסיפה ריחוף, קיבוע בלחיצה, הגדלה ומעקב גלילה. קישורים נוכחיים חושפים aria-current=location, ומחוונים דקורטיביים מוסתרים מטכנולוגיה מסייעת. מעקב הגלילה לעולם אינו מזיז את המיקוד או מכריז על גלילה. בהעדפה לתנועה מופחתת, ערכת העיצוב האופציונלית מבטלת מעברי קווים.

נקודות חיבור לעיצוב

data-uiify-table-of-contentsdata-partdata-selecteddata-highlighteddata-statedata-sidedata-aligndata-positioning--uiify-toc-line-size--uiify-toc-line-max-size--uiify-toc-line-thickness--uiify-toc-row-size--uiify-toc-magnify-radius--uiify-toc-indent--uiify-toc-panel-size--uiify-toc-duration--uiify-toc-level--uiify-toc-index--uiify-toc-pointer
דף זה תורגם אוטומטית וטרם נבדק.