registry:ui · General
anchor — real links, read-only spy
A nav landmark (Anchor) of real fragment links (AnchorItem parts): navigation and smooth scrolling stay native, and JavaScript only reads the scroll position to mark aria-current=location.
$npx jixoai-ui add anchor
https://ui.jixoai.com/r/anchor.json
overview
Overview
Two composed parts, one read-only behavior: the nav landmark, the fragment links, and the active-link pick.
Anchor is the heading-anchor rail (antd's Anchor) as a composed family. The Anchor root renders the nav landmark; each AnchorItem child renders one real fragment link. You compose the items — the family ships no link list and invents no targets.
The root's one behavior beyond the landmark is the active-link pick: a rAF-throttled
scrollspy marks the last target whose top sits at or past the viewport-top line
(offset, default 96px — below sticky headers). Targets derive from the root's
OWN DOM (every child a[href^="#"]) on every read: no registration, so keyed
reorders and conditional items cannot corrupt the spy, and a childList MutationObserver
re-derives them when items enter or leave.
A click keeps native fragment navigation and adds two reversible leases: scroll-margin-top = offset lands the heading clear of sticky headers, and a
temporary tabindex="-1" moves focus onto it (restored on blur). The pick runs
the shared @lib/scroll-spy — anchor answers the simpler which-section
question; the toc family runs the full reading-progress engine. For the axis grammar, see
the universal props concept page.
usage
Usage
Compose AnchorItem links inside Anchor and point each href at an existing page fragment.
<script lang="ts">
import Anchor from '@ui/anchor/anchor.svelte';
import AnchorItem from '@ui/anchor/anchor-item.svelte';
</script>
<Anchor label="on this page">
<AnchorItem href="#what">what it does</AnchorItem>
<AnchorItem href="#pick">the line pick</AnchorItem>
</Anchor>
<!-- the spy reads its targets from the root's OWN DOM (child
a[href^="#"]) — no registration; offset (default 96) drops the
pick line below sticky headers -->anchor
Scroll the band below — the anchor rail beside the workbench marks the section crossing the viewport-top line; the page rail in the site chrome tracks the same fragments.
what it does
A composed list of in-page anchors: clicking navigates the fragment natively (smooth scrolling rides the theme's scroll-behavior), and hovering and focusing behave like any link. The root owns ONE behavior beyond the landmark: the active-link pick.
the line pick
The pick is the LAST target whose top sits at or past the viewport-top line
(offset, default 96px for sticky headers). Targets come from the root's
own DOM: every child a[href^="#"] joins the spy — no registration, no
order dependence.
anchor vs toc
toc measures how much of each block fills the viewport (IoM weights) and draws the rule tracker; anchor answers the simpler which-section question with the same shared pick engine — zero coupling beyond the target ids existing.
filler depth 1 — keeps the last section reachable past the offset line
filler depth 2 — keeps the last section reachable past the offset line
filler depth 3 — keeps the last section reachable past the offset line
filler depth 4 — keeps the last section reachable past the offset line
filler depth 5 — keeps the last section reachable past the offset line
filler depth 6 — keeps the last section reachable past the offset line
filler depth 7 — keeps the last section reachable past the offset line
filler depth 8 — keeps the last section reachable past the offset line
filler depth 9 — keeps the last section reachable past the offset line
filler depth 10 — keeps the last section reachable past the offset line
filler depth 11 — keeps the last section reachable past the offset line
filler depth 12 — keeps the last section reachable past the offset line
filler depth 13 — keeps the last section reachable past the offset line
filler depth 14 — keeps the last section reachable past the offset line
<script lang="ts">
import Anchor from '@ui/anchor/anchor.svelte';
import AnchorItem from '@ui/anchor/anchor-item.svelte';
</script>
<Anchor label="on this page">
<AnchorItem href="#what">what it does</AnchorItem>
<AnchorItem href="#pick">the line pick</AnchorItem>
</Anchor>
<!-- the spy reads its targets from the root's OWN DOM (child
a[href^="#"]) — no registration; offset (default 96) drops the
pick line below sticky headers -->api
Props
The root carries the landmark and the pick line; each item is an ordinary composable anchor. The eight style axes resolve on the root and supply downward — the generated Universal props section below is the shared surface.
Anchor
| Property | Type | Default | Description |
|---|---|---|---|
| label | string | 'on this page' | Accessible name of the navigation landmark. |
| offset | number | 96 | Viewport-top pick line in pixels; also the scroll-margin-top lease on landing. |
| children * | Snippet | — | The AnchorItem links. |
| ...rest | HTMLAttributes | spread | Forwarded to the nav element; class joins the rail atoms. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |
AnchorItem
| Property | Type | Default | Description |
|---|---|---|---|
| href * | string | — | Target fragment, #section-id. |
| child | Snippet | — | Replacement-element escape; receives the merged anchor props — class with the active paint, href, aria-current, rest. |
| children | Snippet | — | Visible link label. |
| ...rest | HTMLAnchorAttributes | spread | Forwarded to the anchor element. |
axes
The eight axes on anchor
anchor carries all eight lanes as a first-time no-own surface (migration census, W3 close D5): an explicit lane stamps its carrier on the nav and supplies downward — AnchorItem parts and nested components read the supply, the broadcast protocol (吃也供, supply-and-consume). Density is the one axis whose paint the rail itself consumes; the other seven stamp-and-supply carriers its paint does not read (theme's .dark bridge flips the raw token layer only — see the theme row).
Reading the table: Property is the axis, Type is the real carrier it stamps on the nav here, Default is the lane default — the named steps, number unit, and consumption on this family are in each description.
| Property | Type | Default | Description |
|---|---|---|---|
| density | data-density rung · --jx-density-coefficient | auto | The one axis whose paint the rail itself consumes. Named steps small · medium · large stamp the rung on the nav and re-scope the channels the paint reads — --jx-stack (rail gap), --jx-hit (link min-height), --jx-inset (inline padding), --jx-text / --jx-line (label). A number is a coefficient: it multiplies the same channels and leaves the rung ambient. The five legacy rung spellings (xs · 2xs · sm · default · lg) also address lanes directly. |
| size | --jx-size-effective → root font-size | auto | Stamps the carrier on the nav. The labels do not follow — they read the density channels --jx-text / --jx-line, not em — so scale the rail with density. Size supplies downward to nested component consumers. Steps small · medium · large; a number is px. |
| shape | --jx-shape-effective · --jx-radius-factor-effective | auto | Carriers stamp on the nav, but the rail paints no corners — nothing consumes them on this family. Steps round · scoop · bevel · notch · square · squircle; no number lane. |
| radius | --jx-radius-effective | auto | Same absence: a left-spine rail has no corner geometry. Steps small · medium · large; a number is px. |
| color | --jx-color-effective | auto | The carrier stamps, but the rail's ink reads the semantic tokens directly — --jx-foreground, --jx-muted-foreground, --jx-primary for the active spine — so a hue lane retunes nothing here. Steps primary · secondary · error · warn · success · info; a number is hue degrees. |
| theme | the .dark class bridge | auto | Stamp-side on this family: a resolved dark puts .dark on the nav and flips the raw token layer (--foreground, --muted-foreground) at that scope — but the rail's ink reads the root-anchored --jx-* aliases, declared only at :root and the stylex theme scopes, which a plain .dark never re-substitutes. The rail follows the site theme, not this lane. Steps light · dark · system; auto inherits the tree. |
| elevation | --jx-elevation-effective | auto | The carrier stamps; the rail paints no shadow. Steps level-1 · level0 · level1 · level2 · level3 · level4 · level5; a number is exact dp. |
| motion | --jx-motion-effective | auto | The carrier stamps; the rail's only transition is the fixed --motion-150 / --motion-ease-out color fade — no motion-kernel consumption. Steps reduced · subtle · normal · expressive; a number is a coefficient. |
Deviations, cited: the eight-axis adoption itself is the census row — anchor was one of
the 13 sweep holes migrated in the W3 close (D5), which retired its declaration-only
density posture (openspec/changes/explicit-props/research/migration-census.md). No §13
renames apply: offset and label are family props, not axis
names.
| Token | Default | Source |
|---|---|---|
| --jx-stack | rung scale × coefficient | density |
| --jx-hit | rung scale × coefficient | density |
| --jx-inset | rung scale × coefficient | density |
| --jx-text | rung scale × coefficient | density |
| --jx-line | rung scale × coefficient | density |
| --jx-hairline | 1px | structural |
<!-- density: named steps re-scope the rail's spacing channels -->
<Anchor label="ambient rail">…</Anchor>
<Anchor label="compact rail" density="small">…</Anchor>
<Anchor label="roomy rail" density="large">…</Anchor>
<!-- theme: supply-side here — a resolved dark stamps the .dark
class bridge on the nav; the rail's ink follows the site theme -->
<Anchor label="dark rail" theme="dark">…</Anchor>
<!-- query(): below the lg viewport rung (64rem) the rail rides
small; at ≥64rem it steps to large. Resize and watch. The
explicit generics pin the case AND the base to the lane (the
bare form infers QueryResult<string> — a type error). -->
<Anchor label="responsive rail" density={query<{ lg: DensityLane }, DensityLane>({ lg: 'large' }, 'small')}>…</Anchor>anchor · the eight axes
SSR paints the base (small); at the lg viewport rung (≥64rem) the rail steps to large — resize across 64rem and watch.
<!-- density: named steps re-scope the rail's spacing channels -->
<Anchor label="ambient rail">…</Anchor>
<Anchor label="compact rail" density="small">…</Anchor>
<Anchor label="roomy rail" density="large">…</Anchor>
<!-- theme: supply-side here — a resolved dark stamps the .dark
class bridge on the nav; the rail's ink follows the site theme -->
<Anchor label="dark rail" theme="dark">…</Anchor>
<!-- query(): below the lg viewport rung (64rem) the rail rides
small; at ≥64rem it steps to large. Resize and watch. The
explicit generics pin the case AND the base to the lane (the
bare form infers QueryResult<string> — a type error). -->
<Anchor label="responsive rail" density={query<{ lg: DensityLane }, DensityLane>({ lg: 'large' }, 'small')}>…</Anchor>a11y
Accessibility
Native links in a named navigation landmark; the scrollspy writes aria-current and never moves focus.
Keyboard
| Key | Action |
|---|---|
| Tab | Moves through the fragment links in composed order; the spy never steals focus |
| Enter | Navigates to the target fragment; focus rides onto the heading (tabindex=-1, restored on blur) |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-label | 'on this page' by default | Names the navigation landmark. |
| aria-current | 'location' on the active item | Announces the section the scrollspy picked; scrolling alone never moves focus. |
| href | #fragment | Real fragment hrefs keep native navigation, deep links, and middle-click. |
Density is also the hit-surface axis here: the link's minimum target height (--jx-hit) measures 32 / 40 / 48px at small / default / large — every rung clears the 24px WCAG 2.5.8 AA target floor.