# Proximity Sidebar

> A React table of contents rail: thin dashes that swell near the pointer and track your section.

- Type: React component (Navigation)
- Access: Free (the source is open to everyone)
- Version: 1.0.1
- Install: `npx wingo-ui@latest add proximity-sidebar`
- Docs: https://wingo-ui.com/components/proximity-sidebar

## Interaction

Scroll through the content and the dash for the current section pulses wide, then settles back once you stop. Move the pointer along the rail to swell the dashes nearest to it, and click a dash to smooth-scroll to its section and update the URL hash (unless updateHash is off). On a phone, touch the rail and slide: the dash under your finger swells, and lifting your finger jumps to it. From the keyboard, Tab onto the rail, move with the arrow keys, Home and End (the focused dash swells), and press Enter to jump.

## Installation

```bash
npx wingo-ui@latest add proximity-sidebar
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/proximity-sidebar.json`

Files:

- `components/ui/proximity-sidebar.tsx`

npm dependencies: `npm i motion`

Wingo UI dependencies (installed automatically):

- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import ProximitySidebar, { type ProximitySection } from "@/components/ui/proximity-sidebar"

const sections = [
  { id: "intro", label: "Introduction", level: 1 },
  { id: "setup", label: "Setup", level: 2 },
  { id: "api", label: "API", kind: "section" },
  { id: "faq", label: "FAQ", kind: "body" },
] satisfies ProximitySection[]

export function Demo() {
  return (
    <aside className="sticky top-20 h-[70vh]">
      <ProximitySidebar sections={sections} side="left" activeOffset={0.4} />
    </aside>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `sections` | `Array<{ id: string; label: string; kind?: "title" \| "subtitle" \| "section" \| "body"; level?: 1 \| 2 \| 3 \| 4 \| 5 \| 6 }>` |  | Ordered section map used for rendering dashes and scroll targeting. Each id must match an element id present in the page. kind (or level 1 to 3) sets the dash length and color; without either, the first heading inside the element decides. |
| `side?` | `"left" \| "right"` | `"left"` | Pins the minimap to the chosen side and flips the dash transform origin to match. |
| `activeOffset?` | `number` | `0.4` | Viewport anchor ratio used to detect the active section while scrolling (0 = top, 1 = bottom). The section closest to that line pulses. |
| `dashColor?` | `string` | `"auto"` | Color of the dashes, any CSS color. Auto draws title and subtitle dashes in the foreground color and the rest in a muted gray; a custom color keeps that hierarchy by drawing the minor dashes at 40%. |
| `activeColor?` | `string` | `"auto"` | Color of the dash for the section you are reading, any CSS color. Auto leaves it the same color as its neighbours, so only its pulse marks it. |
| `dashLength?` | `number` | `110` | Width of the rail in pixels, which is how long a fully swollen dash gets. Every dash length scales with it. |
| `gap?` | `number` | `8` | Space between dashes in pixels. Each dash also owns the band up to its neighbours, so clicks between dashes land on the closest one. |
| `thickness?` | `number` | `1` | Height of each dash in pixels. |
| `reach?` | `number` | `40` | How far in pixels the pointer's pull reaches: dashes within this distance above or below it swell, the nearest the most. |
| `inset?` | `number` | `32` | Horizontal space in pixels between the rail and the edges of the nav. |
| `updateHash?` | `boolean` | `true` | Writes #section-id to the URL (history.replaceState) when a dash is picked. Turn it off for embedded readers and modals. |
| `label?` | `string` | `"Page sections"` | Accessible name of the navigation landmark. |
| `dashLabel?` | `string` | `"Go to {label}"` | Accessible name of each dash; {label} fills in the section label, e.g. "Mergi la {label}". |
| `className?` | `string` | `""` | Additional classes for the outer nav wrapper (data-slot="proximity-sidebar"), which fills its parent's height and centers the stack. Other nav attributes are passed through too. |

## Parts

### ProximitySection (type)

One entry of sections: { id, label, kind?, level? }. id is the element id to scroll to; kind (title, subtitle, section, body) or level (1 to 6) sets the dash length, and without either it is read from the section's first heading.

## Examples

- **Release notes on the right**: side right with an orange active dash; scroll the notes and the current version pulses.
- **Compact rail for a help article**: Short dashes, a tighter gap, thicker lines and a smaller reach for a narrow column.
- **Inside a panel, no URL changes**: updateHash off keeps the address bar still, for a rail inside a dialog, a tab or a side panel.

## See also

- [Table Of Contents](https://wingo-ui.com/components/table-of-contents) ([Markdown](https://wingo-ui.com/components/table-of-contents.md))
- [Scroll Progress](https://wingo-ui.com/components/scroll-progress) ([Markdown](https://wingo-ui.com/components/scroll-progress.md))
- [Sidebar](https://wingo-ui.com/components/sidebar) ([Markdown](https://wingo-ui.com/components/sidebar.md))
- [useScrollState](https://wingo-ui.com/components/use-scroll-state) ([Markdown](https://wingo-ui.com/components/use-scroll-state.md))
- [Back To Top](https://wingo-ui.com/components/back-to-top) ([Markdown](https://wingo-ui.com/components/back-to-top.md))

## Credits

- Inspired by devouringdetails.com

---

Canonical: https://wingo-ui.com/components/proximity-sidebar
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
