Anchor Jump links that track scroll.
<sui-anchor> Menus and navigation StableAbout 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 Web Component React Vue Angular
<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".
Web Component React Vue Angular
Show code
<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.
Web Component React Vue Angular
Show code
<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
Web Component React Vue Angular
Show code
<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.
Web Component React Vue Angular
Show code
<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 Key Action Tab Moves through the section links in order; the current one reads aria-current="location". Enter Follows 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 Property Attribute Type Default Description activeactivestringstring | undefined Id of the highlighted section. Mutable; the scroll-spy and clicks update it. headingheadingstringstring | undefined Rail title and the nav landmark's accessible name (the reference title prop). itemsproperty only SuiAnchorItem[][]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 Event Detail React Vue Description 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 Method Signature Description setFocus()setFocus() => Promise<void>Moves keyboard focus to the current link, or the first one.
Types SuiAnchorItem interface One jump link: the section element id and its label. idstringlabelstring
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.
Every name above is listed with its value and its role in Foundations .
See also