Skip to content
ShineUI
Install

Anchor

Jump links that track scroll.

<sui-anchor>Menus and navigationStable

About

Scroll-spy rail of jump links for long reports and settings pages. It watches the scrolling target, or the window when none is set, highlights the section whose top has passed the spyOffset line, and scrolls smoothly to a section on click. It finds sections by id inside the target.

Usage

<sui-anchor heading="Profit and loss" target="#an-basic-scroll" spy-offset="24"></sui-anchor> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-anchor"); el.items = [ { id: "an-basic-income", label: "Income" }, { id: "an-basic-cogs", label: "Cost of goods sold" }, { id: "an-basic-expenses", label: "Expenses" }, ]; /* SuiAnchorItem[] */ el.addEventListener("suiActiveChange", (e) => console.log(e.detail)); </script>
import { SuiAnchor } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { id: "an-basic-income", label: "Income" }, { id: "an-basic-cogs", label: "Cost of goods sold" }, { id: "an-basic-expenses", label: "Expenses" }, ]; /* SuiAnchorItem[] */ return <SuiAnchor heading="Profit and loss" target="#an-basic-scroll" spyOffset="24" items={items} onSuiActiveChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiAnchor } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { id: "an-basic-income", label: "Income" }, { id: "an-basic-cogs", label: "Cost of goods sold" }, { id: "an-basic-expenses", label: "Expenses" }, ]; // SuiAnchorItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiAnchor heading="Profit and loss" target="#an-basic-scroll" spy-offset="24" :items="items" @sui-active-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiAnchor } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiAnchor], template: `<sui-anchor heading="Profit and loss" target="#an-basic-scroll" spy-offset="24" [(active)]="active" [items]="items"></sui-anchor>`, }) export class Example { active?: string | undefined; items = [ { id: "an-basic-income", label: "Income" }, { id: "an-basic-cogs", label: "Cost of goods sold" }, { id: "an-basic-expenses", label: "Expenses" }, ]; // SuiAnchorItem[] }

Basic

Scroll the report and the rail follows. The rail is a nav named by its heading, each link is a real anchor to its section id, and the current one carries aria-current="location".

Income

Sales$90,740.45
Service revenue$12,300.00
Interest income$140.12
Total income$103,180.57

Cost of goods sold

Materials$28,410.00
Freight$1,890.00
Total cost of goods sold$30,300.00

Expenses

Payroll$41,200.00
Rent$4,800.00
Software$1,260.00
Insurance$980.00
Total expenses$48,240.00
<div style="display:flex;gap:24px;align-items:flex-start"> <sui-anchor heading="Profit and loss" target="#an-basic-scroll" spy-offset="24"></sui-anchor> <div id="an-basic-scroll" tabindex="0" role="group" aria-label="Profit and loss" style="position:relative;height:240px;width:360px;overflow-y:auto;padding:0 16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg)"> <section id="an-basic-income" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Income</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Sales</span><span style="font-feature-settings:'tnum';color:var(--ink)">$90,740.45</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Service revenue</span><span style="font-feature-settings:'tnum';color:var(--ink)">$12,300.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Interest income</span><span style="font-feature-settings:'tnum';color:var(--ink)">$140.12</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total income</span><span style="font-feature-settings:'tnum';color:var(--ink)">$103,180.57</span></div> </section> <section id="an-basic-cogs" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Cost of goods sold</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Materials</span><span style="font-feature-settings:'tnum';color:var(--ink)">$28,410.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Freight</span><span style="font-feature-settings:'tnum';color:var(--ink)">$1,890.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total cost of goods sold</span><span style="font-feature-settings:'tnum';color:var(--ink)">$30,300.00</span></div> </section> <section id="an-basic-expenses" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Expenses</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Payroll</span><span style="font-feature-settings:'tnum';color:var(--ink)">$41,200.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Rent</span><span style="font-feature-settings:'tnum';color:var(--ink)">$4,800.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Software</span><span style="font-feature-settings:'tnum';color:var(--ink)">$1,260.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Insurance</span><span style="font-feature-settings:'tnum';color:var(--ink)">$980.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total expenses</span><span style="font-feature-settings:'tnum';color:var(--ink)">$48,240.00</span></div> </section></div> </div>
// setup(root): properties and events setup(root) { root.querySelector('sui-anchor').items = links('an-basic'); }

Sticky chrome

A page with a sticky header raises spy-offset so a section counts as current only once it clears the chrome, and scroll-pad by the same amount so a clicked section lands below it rather than under it.

Profit and loss, March 2026

Income

Sales$90,740.45
Service revenue$12,300.00
Interest income$140.12
Total income$103,180.57

Cost of goods sold

Materials$28,410.00
Freight$1,890.00
Total cost of goods sold$30,300.00

Expenses

Payroll$41,200.00
Rent$4,800.00
Software$1,260.00
Insurance$980.00
Total expenses$48,240.00
<div style="display:flex;gap:24px;align-items:flex-start"> <sui-anchor id="an-sticky" heading="Report sections" target="#an-sticky-scroll" spy-offset="58" scroll-pad="50"></sui-anchor> <div id="an-sticky-scroll" tabindex="0" role="group" aria-label="Profit and loss" style="position:relative;height:240px;width:360px;overflow-y:auto;padding:0 16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg)"> <div style="position:sticky;top:0;z-index:1;margin:0 -16px;padding:10px 16px;background:var(--surface);border-bottom:1px solid var(--border);font-size:12.5px;font-weight:650;color:var(--ink)">Profit and loss, March 2026</div> <section id="an-sticky-income" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Income</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Sales</span><span style="font-feature-settings:'tnum';color:var(--ink)">$90,740.45</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Service revenue</span><span style="font-feature-settings:'tnum';color:var(--ink)">$12,300.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Interest income</span><span style="font-feature-settings:'tnum';color:var(--ink)">$140.12</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total income</span><span style="font-feature-settings:'tnum';color:var(--ink)">$103,180.57</span></div> </section> <section id="an-sticky-cogs" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Cost of goods sold</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Materials</span><span style="font-feature-settings:'tnum';color:var(--ink)">$28,410.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Freight</span><span style="font-feature-settings:'tnum';color:var(--ink)">$1,890.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total cost of goods sold</span><span style="font-feature-settings:'tnum';color:var(--ink)">$30,300.00</span></div> </section> <section id="an-sticky-expenses" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Expenses</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Payroll</span><span style="font-feature-settings:'tnum';color:var(--ink)">$41,200.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Rent</span><span style="font-feature-settings:'tnum';color:var(--ink)">$4,800.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Software</span><span style="font-feature-settings:'tnum';color:var(--ink)">$1,260.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Insurance</span><span style="font-feature-settings:'tnum';color:var(--ink)">$980.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total expenses</span><span style="font-feature-settings:'tnum';color:var(--ink)">$48,240.00</span></div> </section> </div> </div>
// setup(root): properties and events setup(root) { root.querySelector('#an-sticky').items = links('an-sticky'); }

Tracking the window

Omit target and the rail tracks the window instead of a container, which is the shape a normal long page wants. Scroll this page to watch the highlight move.

Income

Sales$90,740.45
Service revenue$12,300.00
Interest income$140.12
Total income$103,180.57

Cost of goods sold

Materials$28,410.00
Freight$1,890.00
Total cost of goods sold$30,300.00

Expenses

Payroll$41,200.00
Rent$4,800.00
Software$1,260.00
Insurance$980.00
Total expenses$48,240.00
<div style="display:flex;gap:24px;align-items:flex-start"> <div style="position:sticky;top:74px"> <sui-anchor id="an-window" heading="On this report" spy-offset="90"></sui-anchor> </div> <div style="width:360px"> <section id="an-window-income" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Income</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Sales</span><span style="font-feature-settings:'tnum';color:var(--ink)">$90,740.45</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Service revenue</span><span style="font-feature-settings:'tnum';color:var(--ink)">$12,300.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Interest income</span><span style="font-feature-settings:'tnum';color:var(--ink)">$140.12</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total income</span><span style="font-feature-settings:'tnum';color:var(--ink)">$103,180.57</span></div> </section> <section id="an-window-cogs" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Cost of goods sold</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Materials</span><span style="font-feature-settings:'tnum';color:var(--ink)">$28,410.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Freight</span><span style="font-feature-settings:'tnum';color:var(--ink)">$1,890.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total cost of goods sold</span><span style="font-feature-settings:'tnum';color:var(--ink)">$30,300.00</span></div> </section> <section id="an-window-expenses" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Expenses</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Payroll</span><span style="font-feature-settings:'tnum';color:var(--ink)">$41,200.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Rent</span><span style="font-feature-settings:'tnum';color:var(--ink)">$4,800.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Software</span><span style="font-feature-settings:'tnum';color:var(--ink)">$1,260.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Insurance</span><span style="font-feature-settings:'tnum';color:var(--ink)">$980.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total expenses</span><span style="font-feature-settings:'tnum';color:var(--ink)">$48,240.00</span></div> </section></div> </div>
// setup(root): properties and events setup(root) { root.querySelector('#an-window').items = links('an-window'); }

Events

suiActiveChange fires whenever active moves, including when the scroll-spy moves it on its own. suiNavigate fires only on a click, so a router can respond to the click without reacting to every scroll.

Watching Income.

Income

Sales$90,740.45
Service revenue$12,300.00
Interest income$140.12
Total income$103,180.57

Cost of goods sold

Materials$28,410.00
Freight$1,890.00
Total cost of goods sold$30,300.00

Expenses

Payroll$41,200.00
Rent$4,800.00
Software$1,260.00
Insurance$980.00
Total expenses$48,240.00
<div style="display:flex;gap:24px;align-items:flex-start"> <div> <sui-anchor id="an-events" heading="Report sections, events" target="#an-events-scroll" spy-offset="24"></sui-anchor> <div id="an-log" style="margin-top:12px;max-width:170px;font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Watching Income.</div> </div> <div id="an-events-scroll" tabindex="0" role="group" aria-label="Profit and loss" style="position:relative;height:240px;width:360px;overflow-y:auto;padding:0 16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg)"> <section id="an-events-income" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Income</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Sales</span><span style="font-feature-settings:'tnum';color:var(--ink)">$90,740.45</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Service revenue</span><span style="font-feature-settings:'tnum';color:var(--ink)">$12,300.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Interest income</span><span style="font-feature-settings:'tnum';color:var(--ink)">$140.12</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total income</span><span style="font-feature-settings:'tnum';color:var(--ink)">$103,180.57</span></div> </section> <section id="an-events-cogs" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Cost of goods sold</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Materials</span><span style="font-feature-settings:'tnum';color:var(--ink)">$28,410.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Freight</span><span style="font-feature-settings:'tnum';color:var(--ink)">$1,890.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total cost of goods sold</span><span style="font-feature-settings:'tnum';color:var(--ink)">$30,300.00</span></div> </section> <section id="an-events-expenses" style="padding:4px 0 18px"> <h2 style="margin:0 0 8px;font-size:14.5px;font-weight:700;letter-spacing:var(--tracking-tight);color:var(--ink)">Expenses</h2> <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Payroll</span><span style="font-feature-settings:'tnum';color:var(--ink)">$41,200.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Rent</span><span style="font-feature-settings:'tnum';color:var(--ink)">$4,800.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Software</span><span style="font-feature-settings:'tnum';color:var(--ink)">$1,260.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Insurance</span><span style="font-feature-settings:'tnum';color:var(--ink)">$980.00</span></div><div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--hairline);font-size:13px;color:var(--ink-muted)"><span>Total expenses</span><span style="font-feature-settings:'tnum';color:var(--ink)">$48,240.00</span></div> </section></div> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#an-log'); const el = root.querySelector('#an-events'); el.items = links('an-events'); el.addEventListener('suiActiveChange', (e) => { log.textContent = `Spy moved to ${e.detail}.`; }); el.addEventListener('suiNavigate', (e) => { log.textContent = `Clicked ${e.detail}.`; }); }

Keyboard

KeyAction
TabMoves through the section links in order; the current one reads aria-current="location".
EnterFollows the link: emits suiNavigate with the section id and scrolls the section into view under the scroll padding. Space is not a key on a link, so it scrolls the page instead.

API

<sui-anchor>

Properties

PropertyAttributeTypeDefaultDescription
activeactivestringstring | undefinedId of the highlighted section. Mutable; the scroll-spy and clicks update it.
headingheadingstringstring | undefinedRail title and the nav landmark's accessible name (the reference title prop).
itemsproperty onlySuiAnchorItem[][]Links to render: { id, label }[]. Each renders as a real anchor to #id, and the current one carries aria-current="location". Property, not attribute.
scrollPadscroll-padnumber8Space in px left above a section after a click scroll. Set it to the height of any sticky chrome above the content so the section heading is not covered.
spyOffsetspy-offsetnumber60Distance in px from the top of the scroll viewport at which a section counts as current. Set it to the height of any sticky chrome above the content.
targettargetHTMLElement | string | undefinedThe scrolling container: an element, or a CSS selector string. Omit to track the window.

Events

EventDetailReactVueDescription
suiActiveChangestringonSuiActiveChange@sui-active-changeFires whenever active changes, including when the scroll-spy moves it; detail is the section id. A click on a link that is not already current emits this before suiNavigate. Does not bubble.
suiNavigatestringonSuiNavigate@sui-navigateFires when a link is chosen; detail is the section id.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the current link, or the first one.

Types

SuiAnchorItem interfaceOne jump link: the section element id and its label.
idstring
labelstring

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also