# Outline

- Page: https://jiguang132.com/resources/astryx/docs/outline
- Resource: Astryx (https://jiguang132.com/resources/astryx.md)
- All guides: https://jiguang132.com/resources/astryx/docs.md

A table-of-contents sidebar for documentation pages, help centers, wikis, and long settings pages. Use it for navigation within a single page, not for app routes. Features a sliding indicator track that animates to the active heading. The list is a single tab stop: arrow keys move between headings, Home/End jump to the ends, and Enter/Space activate.

## Installation

```bash
npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0
```

Requires React 19+, base CSS and a Theme provider. See [Setup](/resources/astryx/docs/getting-started).

## Outline

A table-of-contents sidebar for documentation pages, help centers, wikis, and long settings pages. Use it for navigation within a single page, not for app routes. Features a sliding indicator track that animates to the active heading. The list is a single tab stop: arrow keys move between headings, Home/End jump to the ends, and Enter/Space activate.

### Usage guidance

- Pass a flat ordered list of headings and let level control indentation.
- Use activeId when custom scroll logic owns the active section.
- Use density="compact" in dense sidebars where vertical space is tight.
- Use useOutlineFromMarkdown or useOutlineFromDOM when headings are generated from content. For linkable Markdown headings, pass the same createMarkdownHeadingLinks() entry to Markdown and Outline; configure headingIdPrefix on that plugin for multiple roots.
- Pass scrollContainerRef when the content scrolls in a split pane, modal, or panel instead of the viewport.
- Set offset to the height of a fixed header that overlays the content, so headings land below it instead of underneath it.
- Avoid: Use Outline for application navigation - use SideNav or TopNav for routes.
- Avoid: Use Outline for expandable hierarchy - use TreeList when nodes need expand and collapse.
- Avoid: Rely on onNavigateEnd to mean "arrived" - it also fires when the user interrupts the scroll.

### Theming API

```json
{
  "targets": [
    {
      "className": "astryx-outline",
      "visualProps": [
        "density"
      ]
    },
    {
      "className": "astryx-outline-indicator"
    },
    {
      "className": "astryx-outline-item",
      "visualProps": [
        "level"
      ],
      "states": [
        "active"
      ]
    }
  ]
}
```

## Outline

Document outline navigation with sliding indicator track. Renders a flat heading list as anchor links with a density variant and scroll-spy active state when uncontrolled.

| Prop | Type | Default | Description |
|---|---|---|---|
| items * | OutlineItem[] |  | Ordered heading items. Each item has id, label, and level (1-6). The id should match the target heading element id. |
| activeId | string |  | Currently active heading id. Providing this prop makes active state controlled and disables built-in scroll-spy. |
| onActiveIdChange | (id: string) => void |  | Called when the active item changes from built-in scroll-spy or from an outline link click. |
| label | string | 'Table of contents' | Accessible label for the nav landmark. |
| density | 'default' \| 'compact' | 'default' | Density variant controlling item padding. 'compact' for dense UIs, 'default' for standard spacing. |
| onNavigateStart | (id: string) => void |  | Called with the item id when navigation begins, before the scroll starts. Pair with onNavigateEnd to drive an arrival effect (flash, ring, pulse) on the target heading. |
| onNavigateEnd | (id: string) => void |  | Called with the item id once the navigation resolves - when the smooth scroll settles, or when reduced motion turns it into an instant jump. Fires exactly once per onNavigateStart, including when the user interrupts the scroll, so a "navigating" state can never leak. |
| offset | number | 0 | Height in px of a fixed header overlaying the top of the scroll root. Shifts both the activation line and the scroll landing by the same amount, so a heading activates exactly where navigating to it puts it - below the header, not underneath it. Composes with each heading's own scroll-margin-top (the header, then the breathing room below it); it does not replace it. Leave at 0 when nothing overlays the content and let scroll-margin-top do the work. |
| scrollContainerRef | React.RefObject<HTMLElement \| null> |  | Scroll container to track, instead of auto-detecting the nearest scrollable ancestor. Use it when content scrolls inside a split pane, modal, or dashboard panel rather than the viewport. |
| hasScrollOnClick | boolean | true | Whether activating an item smooth-scrolls to it. Set to false to own the scrolling yourself (virtualized content, a router): the Outline still updates the active item, the hash, and the navigate callbacks, but performs no scroll. |
| xstyle | StyleXStyles |  | StyleX styles for layout customization. Must be a stylex.create() value. |

### Basic

```tsx
import {Outline} from '@astryxdesign/core/Outline';

const items = [
  {id: 'overview', label: 'Overview', level: 2},
  {id: 'installation', label: 'Installation', level: 2},
  {id: 'theming', label: 'Theming', level: 2},
  {id: 'tokens', label: 'Tokens', level: 3},
  {id: 'accessibility', label: 'Accessibility', level: 2},
];

// Uncontrolled: built-in scroll-spy tracks the topmost visible heading.
<Outline items={items} />;
```

### Compact (density="compact")

```tsx
import {Outline} from '@astryxdesign/core/Outline';

// Dense sidebars use the compact variant; the sliding indicator
// automatically matches the shorter item height.
<Outline items={items} density="compact" />;
```

### Controlled active section

```tsx
import {useState} from 'react';
import {Outline} from '@astryxdesign/core/Outline';

function ControlledOutline() {
  const [activeId, setActiveId] = useState('overview');

  // Providing activeId disables built-in scroll-spy; you own the active state.
  return (
    <Outline
      items={items}
      activeId={activeId}
      onActiveIdChange={setActiveId}
    />
  );
}
```

### Generate items from markdown

```tsx
import {Markdown} from '@astryxdesign/core/Markdown';
import {createMarkdownHeadingLinks} from '@astryxdesign/core/Markdown/plugins';
import {Outline, useOutlineFromMarkdown} from '@astryxdesign/core/Outline';

const markdownRootId = 'guide';
const plugins = [
  createMarkdownHeadingLinks({headingIdPrefix: markdownRootId}),
];

function MarkdownOutline({markdown, isStreaming}) {
  // Pass the same plugin entry and finality to Markdown and Outline. Nested
  // headings consume the shared depth-first sequence, while Outline continues
  // to return root headings only.
  const items = useOutlineFromMarkdown(markdown, {
    plugins,
    isFinal: !isStreaming,
  });
  return (
    <>
      <Markdown id={markdownRootId} plugins={plugins}>{markdown}</Markdown>
      <Outline items={items} />
    </>
  );
}
```

### Scoped scroll container (split pane, modal, panel)

```tsx
import {useRef} from 'react';
import {Outline} from '@astryxdesign/core/Outline';

function PanelDocs() {
  const scrollContainerRef = useRef(null);

  // Without scrollContainerRef the outline auto-detects the nearest scrollable
  // ancestor, which is the viewport here - so the highlight would never move.
  return (
    <div style={{display: 'flex'}}>
      <div ref={scrollContainerRef} style={{overflowY: 'auto', height: 480}}>
        <h2 id="overview">Overview</h2>
        {/* ...content... */}
      </div>
      <Outline items={items} scrollContainerRef={scrollContainerRef} offset={64} />
    </div>
  );
}
```

### Navigate callbacks (flash the heading on arrival)

```tsx
import {useState} from 'react';
import {Outline} from '@astryxdesign/core/Outline';

function FlashOnArrival() {
  const [flashId, setFlashId] = useState(null);

  // onNavigateEnd fires once the smooth scroll settles - and once per
  // onNavigateStart even if the user scrolls away mid-jump, so this never
  // gets stuck.
  return (
    <Outline
      items={items}
      onNavigateStart={() => setFlashId(null)}
      onNavigateEnd={id => setFlashId(id)}
    />
  );
}
```

### Anatomy

- **Outline**: Navigation container for the same-page heading links and indicator.
- **Heading link**: Anchor for one heading, indented by level and marked when active.
- **Label**: Heading text displayed inside its heading link.
- **Indicator track**: Painted vertical rule behind the active indicator.
- **Active indicator**: Sliding bar positioned beside the currently active heading link.


## Examples

- Outline: A document outline with the active section highlighted by the sliding indicator track.
- Outline — Controlled: Drive the active section yourself with activeId and onActiveIdChange. Providing activeId disables the built-in scroll-spy so your own logic owns the highlight.
- Outline — Deep Nesting: Heading levels 1 through 4 map to progressively deeper indentation, so a long document with sub-sections stays scannable.
- Outline — Density: Two density variants control item padding. Use compact for dense sidebars and default for standard documentation layouts. The sliding indicator automatically matches each item height.

---

Index of all JGuang resources: https://jiguang132.com/llms.txt
