---
title: TableOfContent
description: >-
version: 11.17.0
generatedAt: 2026-11-05T09:55:47.139Z
checksum: bc43d12c0027fa958f74ab54519c305c9352b2a3f1a6c7129ac3736b5fee09d2
---

# TableOfContent

## Import

```tsx
import { TableOfContent } from '@dnb/eufemia/extensions'
import '@dnb/eufemia/extensions/table-of-content/style'
```

## Description

Use TableOfContent to help readers find their way around a long page. It provides nested section links, a moving current-section indicator and a disclosure on smaller screens.

Compose links with TableOfContent.Root and TableOfContent.Item. Use TableOfContent.Data for a flat list of headings from a CMS, Markdown processor or application state. Both approaches share navigation, responsive behavior and accessibility. The compound API supports both authored and data-driven content.

## Composition

Use clear labels that match the destination headings. Put an id on each destination and supply that same id to the item. Ids belong to the destination, so the component does not create duplicate document ids.

The data API accepts `{ id, text, level }` entries in document order. Level is the heading level from 1 to 6. Page titles at level 1 are omitted by default, while their sections are included. The lowest numbered heading level after H1 becomes the top list level. maxDepth defaults to two visible levels. Deeper headings still participate in current-section tracking and mark their nearest visible ancestor.

Set includeH1 on Data to include the page title as a link back to the beginning. Supply its existing heading id and a level of 1 in data. The title appears before its sections at the same indentation, becomes current while reading the introduction, and does not count toward maxDepth. H2/H3 nesting stays the same. Title-only pages are supported; maxDepth of 0 still hides the entire navigation.

```tsx
<TableOfContent.Data
  includeH1
  data={[
    { id: 'savings-guide', text: 'Savings guide', level: 1 },
    { id: 'accounts', text: 'Choose an account', level: 2 },
    { id: 'fees', text: 'Interest and fees', level: 3 },
    { id: 'next-steps', text: 'Next steps', level: 2 },
  ]}
/>
```

With Root, compose a top-level Item pointing to the page title, followed by the section items. Root renders the hierarchy you supply and does not need includeH1.

Keep heading ids stable and unique. Inline formatting such as code is supported in text; avoid buttons or links inside link labels. Root inspects direct Item children, including fragments and memoized items. Arbitrary wrapper components are opaque to this discovery; use Data when generating content through another component.

The built-in title and “Show all” label follow the surrounding Provider locale. Set locale on Root or Data to keep one navigation in a specific language. For example, the Portal uses English even when its component examples use Norwegian:

```tsx
<TableOfContent.Data data={headings} locale="en-GB" />
```

This sets the default title to “On this page” and the navigation's lang attribute to en-GB. It leaves sibling components and your heading labels unchanged. A supplied title, aria-label or aria-labelledby still takes precedence; an explicit lang overrides the language attribute.

## Responsive layout and scrolling

Above breakpoint, the list is always expanded with a width of 12rem. Below it, readers can open and close the disclosure. Use stretch to fill the available width, and expanded with onExpandedChange when application state controls the disclosure. The component remembers its disclosure state while moving between screen sizes.

On narrow screens, an open disclosure initially shows at most 10 links. Nested links and an included page title count toward this total. A translated “Show all” button below the list reveals the remaining links and then disappears. Hidden links are removed from keyboard and screen reader navigation. Where supported, browser find-in-page can search these links through hidden="until-found". A match reveals the full list and opens the disclosure without moving keyboard focus. Activating “Show all” moves focus to the first newly revealed link so keyboard readers can continue in order. The full list remains available on desktop.

The disclosure uses [HeightAnimation's openOnFind behavior](/web/components/height-animation/#accessibility). In browsers without native hidden="until-found" support, collapsed links remain hidden and cannot be found until the disclosure and “Show all” are opened manually.

Set maxVisibleItems on Root or Data to choose a different limit. Set it to 0 to show every link. Showing all applies to the current heading list and is preserved while resizing or closing and reopening the disclosure. A different heading list starts with the configured limit. This choice is separate from expandedStorageKey, which remembers whether the disclosure is open.

```tsx
<TableOfContent.Data data={headings} maxVisibleItems={5} />
```

Set expandedStorageKey on Root or Data to remember the disclosure choice across page changes and reloads in the same browser tab. Session storage is the default; set expandedStorage to local when the choice should survive browser restarts. Use the same key across pages that share this preference and different keys for independent navigations. A stored boolean takes precedence over defaultExpanded. Controlled expanded bypasses storage. Desktop expansion does not overwrite the saved mobile choice. If storage is unavailable, the disclosure remains usable without persistence.

```tsx
<TableOfContent.Data
  data={headings}
  expandedStorageKey="article-contents-expanded"
/>
```

For SSR or static HTML, place the pre-hydration script after the prerendered navigation and before application hydration. It restores the saved disclosure styling before first paint; the component adopts the state during hydration. When required by your Content Security Policy, pass the same nonce as styleNonce to Root or Data and as nonce to the script component.

```tsx
import { TableOfContentPreHydrationScript } from '@dnb/eufemia/extensions/table-of-content/TableOfContentPreHydrationScript'

<TableOfContent.Data
  data={headings}
  expandedStorageKey="article-contents-expanded"
/>
<TableOfContentPreHydrationScript />
```

Desktop navigation is sticky by default within its containing block. Set stickyOffset to clear a fixed header, or `sticky={false}` when the application owns positioning. Align its initial position with stickyOffset when it should remain stationary from the first scroll. Mobile disclosure remains in normal document flow. Constrain its height in a flex container when a long table of contents needs its own scrolling area. The current link is revealed inside that area without moving keyboard focus as the reader scrolls. When the desktop list overflows, wheel scrolling stays inside it at both ends. Short lists let the surrounding page scroll.

Use currentSectionOffset for the point where a heading becomes selected, stickyOffset for the TOC position, and CSS scroll-padding-top on the scrolling container for anchor destinations. Set these to the same header clearance to keep headings visible and selected. For example, with a 96px header clearance:

```tsx
<TableOfContent.Data
  data={headings}
  stickyOffset={96}
  currentSectionOffset={96}
/>
```

```scss
html {
  scroll-padding-top: 6rem;
}
```

For a nested scrolling region, apply scroll-padding-top to that region instead of html and measure currentSectionOffset from the viewport top. Use scroll-margin-top on individual destination headings when you cannot style the scrolling container. Scroll padding and scroll margin add together, so avoid applying the full header clearance to both.

## Navigation and current section

Heading lists update immediately by default. Set animateChanges on Root or Data to animate replacements with a short fade and a slide from left to right, staggered by 15ms per link. The stagger is capped for long lists. Keep the component mounted while changing its data or children. The first render, current-section updates and collapsed lists do not animate. Reduced motion switches immediately, and interacting with new links ends the animation. Outgoing links are decorative and hidden from keyboard and screen reader navigation.

```tsx
<TableOfContent.Data data={headings} animateChanges />
```

Following a section link moves the indicator directly to that destination while the browser scrolls, without stopping at the headings passed along the way. The destination stays selected even when there is not enough content below it to reach the selection offset. Automatic tracking resumes on the next scroll, resize, or pointer, touch, wheel or keyboard input. The selected link keeps its text color on hover and activation. The outline follows wrapped labels and changes to their size or content.

Automatic tracking responds to window and nested-container scrolling, resizing, DOM replacements, and visibility changes. If the current heading is hidden or removed dynamically, including immediately after following its link, selection moves to an available heading without requiring another scroll. Hidden sections and headings with hidden, aria-hidden="true", display: none or visibility: hidden are excluded. Revealed or remounted headings participate again automatically; when every tracked heading is hidden or missing, the current link and selection outline are cleared.

The component uses the supplied data or Item children as its link list. Update that data or those children when sections should be added to or removed from navigation. It does not scan the page or generate ids. Update the data when page headings are hidden, removed or restored.

On the initial visit, a URL hash matching a tracked heading takes precedence while that heading is visible below currentSectionOffset and inside its scrolling region. Manual wheel, touch, pointer or keyboard input, later hash/history navigation, or the heading leaving view ends this initial preference. Normal scroll tracking then resumes without reapplying the old hash or changing the URL. Missing, hidden or unrelated hash targets do not affect selection.

Each visible, tracked heading has a distinct selection point, so scrolling down or up selects sections in order without skipping short sections near the bottom. Near the end of a scrollable page or article, selection points for headings that cannot reach currentSectionOffset are fitted into the remaining scroll distance. The final section gets a selection interval before the bottom, rather than requiring the exact last scroll pixel. Earlier sections keep their normal selection threshold. The initial hash preference still applies until manual interaction.

Links retain native fragment navigation, browser history and modified-click behavior. TableOfContent.Item's onClick runs before navigation and destination focus; call event.preventDefault() when the application needs to handle both itself. The scrollable documentation examples contain navigation inside their article panels so trying them does not change the surrounding page position or URL.

For server rendering, all links and the disclosure are present in the HTML. Responsive styles show the desktop list or mobile disclosure before hydration, including custom breakpoints. Supply the heading data during the initial render so the navigation appears with the content. Automatic current-section tracking starts in the browser. Supply currentId to control the current section, or null to clear the indicator and disable automatic tracking.

## Accessibility

- The navigation is a named nav landmark, using its localized title by default. Give multiple tables of contents distinct titles or aria-label values.
- Links use native fragments and semantic nested lists. The current section uses aria-current="location".
- Tab and Shift+Tab move between links. Enter follows a link and moves focus to its destination so keyboard and screen reader users can continue reading there. Pointer activation also moves destination focus. Focus uses preventScroll so it does not interrupt the anchor scroll.
- Automatic destination focus suppresses the destination's Eufemia tooltip, avoiding a tooltip flash on heading permalinks. Direct keyboard focus and pointer hover still show the tooltip normally. This behavior is built into the component.
- Destinations without a tabindex receive a temporary tabindex of -1, removed on blur. Existing focus order is preserved. Heading ids are never duplicated on navigation items.
- The small-screen button exposes aria-expanded and aria-controls. Enter or Space opens it. Collapsed links are hidden from the accessibility tree and keyboard navigation; browser find-in-page can open the disclosure and reveal links beyond maxVisibleItems where hidden="until-found" is supported. Matching content is revealed before the browser scrolls to it, without moving keyboard focus.
- The translated “Show all” button uses aria-controls to identify the list and disappears after activation. Hidden overflow links are excluded from the accessibility tree and keyboard order. Revealing them focuses the first new link; focus stays available when the viewport changes.
- If resizing hides a list that contains keyboard focus, focus moves to its disclosure button. Its chosen expanded state is preserved across breakpoints, and applications can also control that state.
- The current marker and disclosure animation respect the reader's reduced-motion preference. Scrolling never moves keyboard focus.
- Desktop rows use compact 36px targets with 8px between them. Small screens use standard text links. Both layouts have visible keyboard focus, and labels wrap with text zoom.

Use correct heading hierarchy in the page itself. A table of contents supports navigation but cannot repair that hierarchy. Check the finished page with the screen readers and zoom settings supported by your application.

## Relevant links

- [Figma design](https://www.figma.com/design/cdtwQD8IJ7pTeE45U148r1/?node-id=68514-395)
- [Source code](https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-eufemia/src/extensions/table-of-content)
- [Docs code](https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-design-system-portal/src/docs/web/patterns-and-extensions/table-of-content)


## Related components

Table of contents is part of the [Navigation](/web/components/overview/#navigation) category. More components in this category:

- [Breadcrumb](/web/components/breadcrumb/) – to show where someone is and let them move back up the path.
- [Infinite scrolling](/web/components/pagination/infinity-scroller/) – to load more content automatically as people scroll.
- [Pagination](/web/components/pagination/) – to split long content into pages or load more content as people move through it.
- [Sidebar menu](/web/patterns-and-extensions/sidebar-menu/) – for persistent navigation with sections and nested groups.
- [Step indicator](/web/components/step-indicator/) – to show progress through a process with several steps.
- [Tabs](/web/components/tabs/) – to let people switch between related views on the same page.

[See all in Navigation](/web/components/overview/#navigation)


## Explore a desktop article

Scroll inside the savings guide to follow the animated selection outline through three heading levels. The TOC stays on the right below a sticky header. stickyOffset and currentSectionOffset share the header clearance, while scroll-padding-top keeps linked headings visible. This example uses the translated default title, “On this page”. Link clicks scroll only this example. On small screens, the TOC becomes a disclosure above the article.


```tsx
function navigateExample(event: MouseEvent<HTMLElement>, article: HTMLDivElement | null) {
  if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || !article) {
    return;
  }
  const link = (event.target as Element).closest<HTMLAnchorElement>('.dnb-table-of-content a[href^="#"]');
  const target = link && document.getElementById(decodeURIComponent(link.hash.slice(1)));
  if (!target || !article.contains(target)) {
    return;
  }
  event.preventDefault();
  const padding = parseFloat(getComputedStyle(article).scrollPaddingTop) || 0;
  article.scrollTo({
    top: article.scrollTop + target.getBoundingClientRect().top - article.getBoundingClientRect().top - article.clientTop - padding,
    behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth'
  });
}
function useArticleOffset(articleRef: RefObject<HTMLDivElement>, inset = 1) {
  const [offset, setOffset] = useState(0);
  useLayoutEffect(() => {
    const update = () => {
      const element = articleRef.current;
      if (element) {
        setOffset(element.getBoundingClientRect().top + element.clientTop + inset * parseFloat(getComputedStyle(document.documentElement).fontSize));
      }
    };
    update();
    window.addEventListener('scroll', update, {
      capture: true,
      passive: true
    });
    window.addEventListener('resize', update);
    return () => {
      window.removeEventListener('scroll', update, {
        capture: true
      });
      window.removeEventListener('resize', update);
    };
  }, [articleRef, inset]);
  return offset;
}
function DesktopArticleExample() {
  const articleRef = useRef<HTMLDivElement>(null);
  const offset = useArticleOffset(articleRef, 4);
  const sections = [{
    id: 'start',
    text: 'Build your savings plan',
    level: 2,
    body: 'A little planning makes it easier to put money aside. Start with what matters to you, then find a rhythm that fits everyday life.'
  }, {
    id: 'goal',
    text: 'Choose a goal',
    level: 3,
    body: 'A holiday, a new home or a little more peace of mind. Give your savings a purpose and decide when you want to reach it.'
  }, {
    id: 'amount',
    text: 'Find your monthly amount',
    level: 4,
    body: 'Look at what comes in and what goes out. Choose an amount you can keep saving, even in a busy month.'
  }, {
    id: 'buffer',
    text: 'Keep a safety buffer',
    level: 4,
    body: 'Set aside a little for unexpected expenses. Keep this money easy to reach when you need it.'
  }, {
    id: 'account',
    text: 'Find the right account',
    level: 2,
    body: 'Match the account to your goal. Compare interest rates, fees and how quickly you can access your savings.'
  }, {
    id: 'access',
    text: 'Access to your money',
    level: 3,
    body: 'Some accounts let you withdraw whenever you want. Others reward you for leaving the money untouched.'
  }, {
    id: 'interest',
    text: 'Interest and fees',
    level: 3,
    body: 'Check the effective interest rate and any withdrawal fees. Small differences add up over time.'
  }, {
    id: 'compare',
    text: 'Compare your options',
    level: 4,
    body: 'Read the terms side by side and choose an account that supports how you plan to save.'
  }, {
    id: 'routine',
    text: 'Make saving a habit',
    level: 2,
    body: 'A regular transfer can help your savings grow without adding another task to your day.'
  }, {
    id: 'transfer',
    text: 'Set up a regular transfer',
    level: 3,
    body: 'Choose a date just after payday. Start small and increase the amount when your budget allows it.'
  }, {
    id: 'adjust',
    text: 'Adjust when life changes',
    level: 3,
    body: 'Your savings plan can change with you. Lower the amount for a while or save a little more when you can.'
  }, {
    id: 'review',
    text: 'Review your progress',
    level: 2,
    body: 'Take a moment to see how far you have come. Regular check-ins help you stay on track.'
  }, {
    id: 'milestones',
    text: 'Celebrate small milestones',
    level: 3,
    body: 'Notice each step forward. Consistent contributions matter more than a perfect month.'
  }, {
    id: 'next',
    text: 'Plan your next step',
    level: 3,
    body: 'When you reach your goal, decide what comes next. Keep the habits that worked for you.'
  }];
  return <ScrollView ref={articleRef} className={styles.desktopScroll} interactive role="region" aria-label="Example savings plan with a sticky header">
              <div className={styles.desktopHeader}>
                <P>Sticky header</P>
              </div>
              <div className={styles.desktopLayout}>
                <div className={styles.desktopContent}>
                  {sections.map(({
          id,
          text,
          level,
          body
        }) => {
          const Title = level === 2 ? H2 : level === 3 ? H3 : H4;
          return <section key={id}>
                        <Title id={'toc-desktop-' + id}>{text}</Title>
                        <P>{body}</P>
                      </section>;
        })}
                </div>
                <TableOfContent.Data locale="en-GB" aria-label="Savings plan" breakpoint="small" defaultExpanded maxDepth={3} stickyOffset="4rem" currentSectionOffset={offset} data={sections.map(({
        id,
        text,
        level
      }) => ({
        id: 'toc-desktop-' + id,
        text,
        level
      }))} onClick={event => navigateExample(event, articleRef.current)} />
              </div>
            </ScrollView>;
}
render(<DesktopArticleExample />);
```


## Follow a scrollable article

Scroll inside the article or follow a section link. The current link follows visible headings inside the scroll container, and keyboard focus moves to the destination when a link is activated.


```tsx
function navigateExample(event: MouseEvent<HTMLElement>, article: HTMLDivElement | null) {
  if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || !article) {
    return;
  }
  const link = (event.target as Element).closest<HTMLAnchorElement>('.dnb-table-of-content a[href^="#"]');
  const target = link && document.getElementById(decodeURIComponent(link.hash.slice(1)));
  if (!target || !article.contains(target)) {
    return;
  }
  event.preventDefault();
  const padding = parseFloat(getComputedStyle(article).scrollPaddingTop) || 0;
  article.scrollTo({
    top: article.scrollTop + target.getBoundingClientRect().top - article.getBoundingClientRect().top - article.clientTop - padding,
    behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth'
  });
}
function useArticleOffset(articleRef: RefObject<HTMLDivElement>, inset = 1) {
  const [offset, setOffset] = useState(0);
  useLayoutEffect(() => {
    const update = () => {
      const element = articleRef.current;
      if (element) {
        setOffset(element.getBoundingClientRect().top + element.clientTop + inset * parseFloat(getComputedStyle(document.documentElement).fontSize));
      }
    };
    update();
    window.addEventListener('scroll', update, {
      capture: true,
      passive: true
    });
    window.addEventListener('resize', update);
    return () => {
      window.removeEventListener('scroll', update, {
        capture: true
      });
      window.removeEventListener('resize', update);
    };
  }, [articleRef, inset]);
  return offset;
}
function ArticleExample() {
  const id = 'toc-article';
  const articleRef = useRef<HTMLDivElement>(null);
  const offset = useArticleOffset(articleRef);
  return <div className={styles.example + ' ' + styles.exampleRight}>
              <ScrollView ref={articleRef} className={styles.article} interactive role="region" aria-label="Example savings article">
                <section>
                  <H2 id={id + '-goal'}>Set a savings goal</H2>
                  <P>
                    Give your savings a purpose. A clear goal helps you
                    decide how much to set aside and when you need the
                    money.
                  </P>
                  <H3 id={id + '-amount'}>Choose an amount</H3>
                  <P>
                    Start with an amount that fits your everyday budget.
                    You can adjust it as your situation changes.
                  </P>
                </section>
                <section>
                  <H2 id={id + '-account'}>Find the right account</H2>
                  <P>
                    Compare access to your money, interest and fees. Keep
                    an emergency buffer available for unexpected expenses.
                  </P>
                </section>
                <section>
                  <H2 id={id + '-review'}>Review your progress</H2>
                  <P>
                    Check your progress regularly. Small, consistent
                    contributions can help you build a useful buffer over
                    time.
                  </P>
                </section>
              </ScrollView>
              <TableOfContent.Data onClick={event => navigateExample(event, articleRef.current)} title="Plan your savings" defaultExpanded currentSectionOffset={offset} data={[{
      id: id + '-goal',
      text: 'Set a savings goal',
      level: 2
    }, {
      id: id + '-amount',
      text: 'Choose an amount',
      level: 3
    }, {
      id: id + '-account',
      text: 'Find the right account',
      level: 2
    }, {
      id: id + '-review',
      text: 'Review your progress',
      level: 2
    }]} />
            </div>;
}
render(<ArticleExample />);
```


## Generate links from headings

Data accepts a flat list in document order. Level 1 represents the page title and does not appear in the navigation. Scroll the example article to move the selected outline. A deeper current section marks its closest visible ancestor.


```tsx
function navigateExample(event: MouseEvent<HTMLElement>, article: HTMLDivElement | null) {
  if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || !article) {
    return;
  }
  const link = (event.target as Element).closest<HTMLAnchorElement>('.dnb-table-of-content a[href^="#"]');
  const target = link && document.getElementById(decodeURIComponent(link.hash.slice(1)));
  if (!target || !article.contains(target)) {
    return;
  }
  event.preventDefault();
  const padding = parseFloat(getComputedStyle(article).scrollPaddingTop) || 0;
  article.scrollTo({
    top: article.scrollTop + target.getBoundingClientRect().top - article.getBoundingClientRect().top - article.clientTop - padding,
    behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth'
  });
}
function useArticleOffset(articleRef: RefObject<HTMLDivElement>, inset = 1) {
  const [offset, setOffset] = useState(0);
  useLayoutEffect(() => {
    const update = () => {
      const element = articleRef.current;
      if (element) {
        setOffset(element.getBoundingClientRect().top + element.clientTop + inset * parseFloat(getComputedStyle(document.documentElement).fontSize));
      }
    };
    update();
    window.addEventListener('scroll', update, {
      capture: true,
      passive: true
    });
    window.addEventListener('resize', update);
    return () => {
      window.removeEventListener('scroll', update, {
        capture: true
      });
      window.removeEventListener('resize', update);
    };
  }, [articleRef, inset]);
  return offset;
}
function IntegrationExample() {
  const articleRef = useRef<HTMLDivElement>(null);
  const offset = useArticleOffset(articleRef);
  return <div className={styles.example + ' ' + styles.exampleRight}>
              <ScrollView ref={articleRef} className={styles.article} interactive role="region" aria-label="Example integration guide">
                <section>
                  <H2 id="toc-data-install">Install the package</H2>
                  <P>
                    Add Eufemia to your application and load its styles.
                  </P>
                </section>
                <section>
                  <H2 id="toc-data-root">TableOfContent.Root</H2>
                  <P>
                    Compose section links or supply a flat list of
                    headings.
                  </P>
                </section>
                <section>
                  <H3 id="toc-data-props">Properties and configuration</H3>
                  <P>
                    Adjust the depth and sticky offset to fit your page.
                  </P>
                  <H4 id="toc-data-internals">Implementation details</H4>
                  <P>
                    Headings below the visible depth highlight their
                    nearest visible ancestor.
                  </P>
                </section>
                <section>
                  <H2 id="toc-data-accessibility">
                    Test accessibility in your application
                  </H2>
                  <P>
                    Check keyboard navigation and descriptive section
                    labels.
                  </P>
                </section>
              </ScrollView>
              <TableOfContent.Data onClick={event => navigateExample(event, articleRef.current)} locale="en-GB" aria-label="Integration guide" currentSectionOffset={offset} maxDepth={2} data={[{
      id: 'toc-data-title',
      text: 'Integration guide',
      level: 1
    }, {
      id: 'toc-data-install',
      text: 'Install the package',
      level: 2
    }, {
      id: 'toc-data-root',
      text: <code>TableOfContent.Root</code>,
      level: 2
    }, {
      id: 'toc-data-props',
      text: 'Properties and configuration',
      level: 3
    }, {
      id: 'toc-data-internals',
      text: 'Implementation details',
      level: 4
    }, {
      id: 'toc-data-accessibility',
      text: 'Test accessibility in your application',
      level: 2
    }]} />
            </div>;
}
render(<IntegrationExample />);
```


## Compose section links

Use Root and nested Item elements when you own the markup. The selected section below is controlled so the example stays independent of the documentation page's scrolling.


```tsx
render(<TableOfContent.Root title="Your savings guide" currentId="toc-declarative-interest">
        <TableOfContent.Item id="toc-declarative-start" text="Get started" />
        <TableOfContent.Item id="toc-declarative-accounts" text="Choose an account">
          <TableOfContent.Item id="toc-declarative-interest" text="Interest and fees" />
        </TableOfContent.Item>
        <TableOfContent.Item id="toc-declarative-next" text="Next steps" />
      </TableOfContent.Root>)
```

<!-- eufemia-properties-usage:start -->
Some properties provide flexibility for specific integrations. Follow the component's usage guidance. Customizations beyond the documented patterns need a use case approved by DNB UX. [Read about intended use and customization](/web/styling-and-themes/configuration/component-properties/#intended-use-and-customization).
<!-- eufemia-properties-usage:end -->

## TableOfContent.Root


```json
{
  "props": {
    "children": {
      "doc": "Nested TableOfContent.Item elements. Fragments and memoized items are supported. Use Data for generated content.",
      "type": "React.ReactNode",
      "status": "optional"
    },
    "title": {
      "doc": "Visible title and default accessible name. Translated through locale or the Provider locale.",
      "type": "React.ReactNode",
      "defaultValue": "\"On this page\"",
      "status": "optional"
    },
    "locale": {
      "doc": "Language of the built-in title and show-all label. Overrides the Provider locale for this navigation only. Also sets its lang attribute unless lang is supplied. Custom titles and accessible names take precedence.",
      "type": "string",
      "status": "optional"
    },
    "currentId": {
      "doc": "Controlled current section id. Omit to detect it while scrolling. Set null to clear it. A section below maxDepth marks its nearest visible ancestor.",
      "type": [
        "string",
        "null"
      ],
      "status": "optional"
    },
    "currentSectionOffset": {
      "doc": "Distance in pixels from the viewport top where a heading becomes current. Match the height of your sticky header.",
      "type": "number",
      "defaultValue": "0",
      "status": "optional"
    },
    "maxDepth": {
      "doc": "Number of visible levels, starting at the highest heading level below h1. Deeper headings remain tracked.",
      "type": "number",
      "defaultValue": "2",
      "status": "optional"
    },
    "maxVisibleItems": {
      "doc": "Maximum number of links initially shown below the breakpoint, including nested links and an included page title. Set 0 to show every link. Desktop always shows all links.",
      "type": "number",
      "defaultValue": "10",
      "status": "optional"
    },
    "breakpoint": {
      "doc": "Eufemia breakpoint above which the navigation is always expanded. At or below it, a disclosure button controls the list. Defaults to large (72em, normally 1152px); Provider breakpoint overrides are respected.",
      "type": [
        "\"small\"",
        "\"medium\"",
        "\"large\"",
        "\"x-large\"",
        "\"xx-large\""
      ],
      "defaultValue": "\"large\"",
      "status": "optional"
    },
    "styleNonce": {
      "doc": "CSP nonce for the inline styles used before hydration.",
      "type": "string",
      "status": "optional"
    },
    "stretch": {
      "doc": "Use the available width. Otherwise the expanded sidebar is 12rem wide.",
      "type": "boolean",
      "defaultValue": "false",
      "status": "optional"
    },
    "sticky": {
      "doc": "Keep the desktop navigation visible while its containing content scrolls. Set false when the application positions it.",
      "type": "boolean",
      "defaultValue": "true",
      "status": "optional"
    },
    "stickyOffset": {
      "doc": "Distance from the top of the scroll container. Numbers use pixels; strings accept CSS lengths.",
      "type": [
        "number",
        "string"
      ],
      "defaultValue": "0",
      "status": "optional"
    },
    "animateChanges": {
      "doc": "Animate heading-list changes with a short fade and staggered slide from the left. Initial rendering, section selection and collapsed lists are not animated. Respects reduced motion.",
      "type": "boolean",
      "defaultValue": "false",
      "status": "optional"
    },
    "expanded": {
      "doc": "Controlled disclosure state below the breakpoint.",
      "type": "boolean",
      "status": "optional"
    },
    "defaultExpanded": {
      "doc": "Initial disclosure state below the breakpoint.",
      "type": "boolean",
      "defaultValue": "false",
      "status": "optional"
    },
    "expandedStorageKey": {
      "doc": "Storage key used to remember the disclosure state across navigation and reloads. Stored booleans override defaultExpanded. Controlled expanded does not read or write storage.",
      "type": "string",
      "status": "optional"
    },
    "expandedStorage": {
      "doc": "Storage used with expandedStorageKey. Session storage is recommended for navigation UI state.",
      "type": [
        "\"session\"",
        "\"local\""
      ],
      "defaultValue": "\"session\"",
      "status": "optional"
    },
    "ref": {
      "doc": "Reference to the navigation element.",
      "type": "React.Ref<HTMLElement>",
      "status": "optional"
    },
    "[HTML attributes]": {
      "doc": "Standard nav attributes, including className, style, aria-label and aria-labelledby. A supplied accessible name takes precedence over the visible title.",
      "type": "Various",
      "status": "optional"
    },
    "[Space properties]": {
      "doc": "Supports spacing properties.",
      "type": "Various",
      "status": "optional"
    }
  },
  "usageGuidance": "Some properties provide flexibility for specific integrations. Follow the component's usage guidance. Customizations beyond the documented patterns need a use case approved by DNB UX. [Read about intended use and customization](/web/styling-and-themes/configuration/component-properties/#intended-use-and-customization).",
  "showDefaultValue": true
}
```


## TableOfContent.Data


```json
{
  "props": {
    "data": {
      "doc": "Flat array of { id, text, level } in document order. Ids identify existing destinations and must be unique on the page. Text accepts inline React content. Level is the heading level from 1 to 6; h1 headings are tracked and shown when includeH1 is enabled.",
      "type": "Array<TableOfContentSection>",
      "status": "required"
    },
    "[Root properties]": {
      "doc": "Supports TableOfContent.Root properties except children.",
      "type": "Various",
      "status": "optional"
    },
    "includeH1": {
      "doc": "Include level-1 page titles as links before their sections, without adding indentation or counting toward maxDepth.",
      "type": "boolean",
      "defaultValue": "false",
      "status": "optional"
    }
  },
  "showDefaultValue": true
}
```


## TableOfContent.Item


```json
{
  "props": {
    "id": {
      "doc": "Unique id of the heading or section to navigate to. It is not applied to the list item.",
      "type": "string",
      "status": "required"
    },
    "text": {
      "doc": "Readable link label. Supports inline formatting such as code. Avoid interactive content inside labels.",
      "type": "React.ReactNode",
      "status": "required"
    },
    "children": {
      "doc": "Nested TableOfContent.Item elements.",
      "type": "React.ReactNode",
      "status": "optional"
    },
    "[HTML attributes]": {
      "doc": "Standard list-item attributes, including className and style.",
      "type": "Various",
      "status": "optional"
    }
  },
  "showDefaultValue": true
}
```


## TableOfContentPreHydrationScript

Import this helper from `@dnb/eufemia/extensions/table-of-content/TableOfContentPreHydrationScript`. It restores the saved disclosure styling before first paint in SSR and static pages. Place it after the navigation markup and before application hydration. It accepts standard script attributes, including nonce, except children and dangerouslySetInnerHTML.

The same module exports getPreHydrationScript for HTML templates assembled outside React. See [disclosure persistence](/web/patterns-and-extensions/table-of-content/#responsive-layout-and-scrolling) for placement and usage.

## Translations


```json
{
  "locales": [
    "da-DK",
    "en-GB",
    "nb-NO",
    "sv-SE"
  ],
  "entries": {
    "TableOfContent.showAll": {
      "nb-NO": "Vis alle",
      "en-GB": "Show all",
      "sv-SE": "Visa alla",
      "da-DK": "Vis alle"
    },
    "TableOfContent.title": {
      "nb-NO": "På denne siden",
      "en-GB": "On this page",
      "sv-SE": "På den här sidan",
      "da-DK": "På denne side"
    }
  }
}
```

## TableOfContent.Root and TableOfContent.Data


```json
{
  "props": {
    "onExpandedChange": {
      "doc": "Called with the requested disclosure state after button activation or browser find-in-page. Update expanded when controlling the state.",
      "type": "(expanded: boolean) => void",
      "status": "optional"
    }
  }
}
```


## TableOfContent.Item


```json
{
  "props": {
    "onClick": {
      "doc": "Called before native fragment navigation and destination focus. Call event.preventDefault() to handle navigation yourself.",
      "type": "React.MouseEventHandler<HTMLAnchorElement>",
      "status": "optional"
    }
  }
}
```
