Table of Contents
In-page navigation built from a target container's headings (or explicit items), with IntersectionObserver active-highlight, smooth scroll and aria-current.
Preview
Getting started
Install the package and register the service provider. Scroll the page to watch the active section update in the navigation beside this content.
Requirements
You will need a recent PHP runtime and a compatible Laravel application before continuing with the setup steps.
Configuration
Publish the configuration file and adjust the defaults to match your environment. Each option is documented inline for clarity.
Environment variables
Sensitive values belong in your environment file rather than committed configuration so they never leak into source control.
Usage
Drop the component into any Blade view and it will smooth-scroll to each section while highlighting the heading currently in view.
<div class="grid gap-8 md:grid-cols-[1fr_220px]">
<article id="toc-demo" class="space-y-4">
<section>
<h2 id="getting-started" class="text-xl font-semibold text-foreground">{{ __('Getting started') }}</h2>
<p class="text-muted-foreground">{{ __('Install the package and register the service provider. Scroll the page to watch the active section update in the navigation beside this content.') }}</p>
</section>
<section>
<h3 id="requirements" class="text-lg font-medium text-foreground">{{ __('Requirements') }}</h3>
<p class="text-muted-foreground">{{ __('You will need a recent PHP runtime and a compatible Laravel application before continuing with the setup steps.') }}</p>
</section>
<section>
<h2 id="configuration" class="text-xl font-semibold text-foreground">{{ __('Configuration') }}</h2>
<p class="text-muted-foreground">{{ __('Publish the configuration file and adjust the defaults to match your environment. Each option is documented inline for clarity.') }}</p>
</section>
<section>
<h3 id="environment" class="text-lg font-medium text-foreground">{{ __('Environment variables') }}</h3>
<p class="text-muted-foreground">{{ __('Sensitive values belong in your environment file rather than committed configuration so they never leak into source control.') }}</p>
</section>
<section>
<h2 id="usage" class="text-xl font-semibold text-foreground">{{ __('Usage') }}</h2>
<p class="text-muted-foreground">{{ __('Drop the component into any Blade view and it will smooth-scroll to each section while highlighting the heading currently in view.') }}</p>
</section>
</article>
<x-ui.table-of-contents target="#toc-demo" class="md:sticky md:top-4 md:self-start" />
</div>
Installation
php artisan ui:add table-of-contents
Note
This component ships an Alpine behavior module at
resources/js/ui/table-of-contents.js. Import it once from your bundle so it registers on alpine:init:
import './table-of-contents.js';
Registry contract
php artisan ui:add table-of-contents
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/table-of-contents.blade.php -
resources/js/ui/table-of-contents.js
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2npm: alpinejs
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Table of Contents (`table-of-contents`)
In-page navigation built from a target container's headings (or explicit items), with IntersectionObserver active-highlight, smooth scroll and aria-current.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add table-of-contents
```
## Usage
```blade
<div class="grid gap-8 md:grid-cols-[1fr_220px]">
<article id="toc-demo" class="space-y-4">
<section>
<h2 id="getting-started" class="text-xl font-semibold text-foreground">{{ __('Getting started') }}</h2>
<p class="text-muted-foreground">{{ __('Install the package and register the service provider. Scroll the page to watch the active section update in the navigation beside this content.') }}</p>
</section>
<section>
<h3 id="requirements" class="text-lg font-medium text-foreground">{{ __('Requirements') }}</h3>
<p class="text-muted-foreground">{{ __('You will need a recent PHP runtime and a compatible Laravel application before continuing with the setup steps.') }}</p>
</section>
<section>
<h2 id="configuration" class="text-xl font-semibold text-foreground">{{ __('Configuration') }}</h2>
<p class="text-muted-foreground">{{ __('Publish the configuration file and adjust the defaults to match your environment. Each option is documented inline for clarity.') }}</p>
</section>
<section>
<h3 id="environment" class="text-lg font-medium text-foreground">{{ __('Environment variables') }}</h3>
<p class="text-muted-foreground">{{ __('Sensitive values belong in your environment file rather than committed configuration so they never leak into source control.') }}</p>
</section>
<section>
<h2 id="usage" class="text-xl font-semibold text-foreground">{{ __('Usage') }}</h2>
<p class="text-muted-foreground">{{ __('Drop the component into any Blade view and it will smooth-scroll to each section while highlighting the heading currently in view.') }}</p>
</section>
</article>
<x-ui.table-of-contents target="#toc-demo" class="md:sticky md:top-4 md:self-start" />
</div>
```
## Props
- `target` (mixed|null, default `null`) — CSS selector for the container whose headings are collected; ignored when items is supplied explicitly.
- `selector` (string, default `h2, h3`) — CSS selector used to find headings inside the target container.
- `items` (array, default `[]`) — Explicit list of { label, href } entries, used instead of deriving items from headings.
- `title` (mixed|null, default `null`) — Heading shown above the list.
- `label` (mixed|null, default `null`) — Accessible label for the navigation landmark when no visible title is shown.
- `variant` (list|rail, default `list`) — Visual layout: list (vertical) or another supported rail style.
- `root` (mixed|null, default `null`) — CSS selector for the IntersectionObserver's scrolling root, when it is not the viewport.
- `offset` (int, default `0`) — Pixel offset applied to the active-section detection, to account for a sticky header.
## Use when
- Use to orient users and help them move across pages, sections, or commands.
- Giving readers a jump list of the current page's headings, with the active section highlighted as they scroll.
- Building a long-form article, docs, or guide page that benefits from an on-this-page rail.
## Avoid when
- Do not hide primary wayfinding in novelty interactions or deep nested structures if straightforward navigation would be clearer.
- The page is short enough to scan without a jump list.
- The list should be a fixed, hand-authored set of links unrelated to the page's own headings; use navigation-menu or sidebar instead.
## Anti-patterns
- Hiding primary wayfinding in novelty interactions
## Rules
- Use the `<brok:table-of-contents>` tag (or `<x-ui.table-of-contents>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/table-of-contents
- Registry JSON (files, props, contract): https://brokui.dev/r/open/table-of-contents.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
{{-- `variant="rail"` with explicit items: the docs-page "On this page" rail.
Server-rendered links, so deep links resolve before Alpine boots. --}}
<div class="grid gap-8 md:grid-cols-[1fr_220px]">
<article class="space-y-4">
<section>
<h2 id="rail-overview" class="text-xl font-semibold text-foreground">{{ __('Overview') }}</h2>
<p class="text-muted-foreground">{{ __('The rail marks the active section on a hairline down its start edge and keeps the title small, so it sits beside long reading content without competing with it.') }}</p>
</section>
<section>
<h3 id="rail-props" class="text-lg font-medium text-foreground">{{ __('Props') }}</h3>
<p class="text-muted-foreground">{{ __('Pass items explicitly or point the component at a container to scan its headings.') }}</p>
</section>
<section>
<h2 id="rail-accessibility" class="text-xl font-semibold text-foreground">{{ __('Accessibility') }}</h2>
<p class="text-muted-foreground">{{ __('Links carry aria-current for the active section, and choosing one moves focus to the heading so the next Tab continues into the section.') }}</p>
</section>
</article>
<x-ui.table-of-contents
variant="rail"
:items="[
['id' => 'rail-overview', 'label' => __('Overview'), 'level' => 2],
['id' => 'rail-props', 'label' => __('Props'), 'level' => 3],
['id' => 'rail-accessibility', 'label' => __('Accessibility'), 'level' => 2],
]"
class="md:sticky md:top-4 md:self-start"
/>
</div>
@php
$items = [
['id' => 'toc-pane-introduction', 'label' => __('Introduction')],
['id' => 'toc-pane-features', 'label' => __('Features')],
['id' => 'toc-pane-conclusion', 'label' => __('Conclusion')],
];
@endphp
{{-- Sections inside a scrolling pane: `root="auto"` observes that pane rather
than the viewport, and `offset` trims the active band for a fixed header. --}}
<div class="grid gap-8 md:grid-cols-[1fr_200px]">
<div class="max-h-80 space-y-0 overflow-auto" tabindex="0" role="region" aria-label="{{ __('Sections') }}">
<section id="toc-pane-introduction" class="min-h-64 space-y-2 py-4">
<h2 class="text-xl font-semibold text-foreground">{{ __('Introduction') }}</h2>
<p class="text-muted-foreground">{{ __('The list tracks which section of this pane is in view and highlights the matching link. Scroll the pane to see the active item change.') }}</p>
</section>
<section id="toc-pane-features" class="min-h-64 space-y-2 py-4">
<h2 class="text-xl font-semibold text-foreground">{{ __('Features') }}</h2>
<p class="text-muted-foreground">{{ __('An IntersectionObserver runs against the pane itself, so there is no scroll polling; the end of the pane marks the last section.') }}</p>
</section>
<section id="toc-pane-conclusion" class="min-h-64 space-y-2 py-4">
<h2 class="text-xl font-semibold text-foreground">{{ __('Conclusion') }}</h2>
<p class="text-muted-foreground">{{ __('Click any link to smooth-scroll the pane to that section.') }}</p>
</section>
</div>
<x-ui.table-of-contents variant="rail" root="auto" :items="$items" :title="__('On this page')" :label="__('Page sections')" class="md:sticky md:top-4 md:self-start" />
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| target | mixed | null | null | CSS selector for the container whose headings are collected; ignored when items is supplied explicitly. |
| selector | string | h2, h3 | CSS selector used to find headings inside the target container. |
| items | array | [] | Explicit list of { label, href } entries, used instead of deriving items from headings. |
| title | mixed | null | null | Heading shown above the list. |
| label | mixed | null | null | Accessible label for the navigation landmark when no visible title is shown. |
| variant | list | rail | list | Visual layout: list (vertical) or another supported rail style. |
| root | mixed | null | null | CSS selector for the IntersectionObserver's scrolling root, when it is not the viewport. |
| offset | int | 0 | Pixel offset applied to the active-section detection, to account for a sticky header. |
Slots
default—
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Derives entries from the target container's headings matching selector, unless an explicit items array is supplied.
- An IntersectionObserver tracks which heading is currently in view and highlights the matching link as active while scrolling.
- Clicking a link smooth-scrolls to its heading instead of jumping instantly.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized.
Guidance
Orient users and move between destinations.
Use when
- Use to orient users and help them move across pages, sections, or commands.
- Giving readers a jump list of the current page's headings, with the active section highlighted as they scroll.
- Building a long-form article, docs, or guide page that benefits from an on-this-page rail.
Avoid when
- Do not hide primary wayfinding in novelty interactions or deep nested structures if straightforward navigation would be clearer.
- The page is short enough to scan without a jump list.
- The list should be a fixed, hand-authored set of links unrelated to the page's own headings; use navigation-menu or sidebar instead.
Use instead
- Visible links and local navigation
Anti-patterns
- Hiding primary wayfinding in novelty interactions
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- native
- Focus
managed
- The active link is marked aria-current rather than relying on colour alone, and the root is a labelled navigation landmark.
- Smooth scrolling honours prefers-reduced-motion for users who have motion reduction enabled.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="table-of-contents-{{ $record->id }}">
<div class="grid gap-8 md:grid-cols-[1fr_220px]">
<article id="toc-demo" class="space-y-4">
<section>
<h2 id="getting-started" class="text-xl font-semibold text-foreground">{{ __('Getting started') }}</h2>
<p class="text-muted-foreground">{{ __('Install the package and register the service provider. Scroll the page to watch the active section update in the navigation beside this content.') }}</p>
</section>
<section>
<h3 id="requirements" class="text-lg font-medium text-foreground">{{ __('Requirements') }}</h3>
<p class="text-muted-foreground">{{ __('You will need a recent PHP runtime and a compatible Laravel application before continuing with the setup steps.') }}</p>
</section>
<section>
<h2 id="configuration" class="text-xl font-semibold text-foreground">{{ __('Configuration') }}</h2>
<p class="text-muted-foreground">{{ __('Publish the configuration file and adjust the defaults to match your environment. Each option is documented inline for clarity.') }}</p>
</section>
<section>
<h3 id="environment" class="text-lg font-medium text-foreground">{{ __('Environment variables') }}</h3>
<p class="text-muted-foreground">{{ __('Sensitive values belong in your environment file rather than committed configuration so they never leak into source control.') }}</p>
</section>
<section>
<h2 id="usage" class="text-xl font-semibold text-foreground">{{ __('Usage') }}</h2>
<p class="text-muted-foreground">{{ __('Drop the component into any Blade view and it will smooth-scroll to each section while highlighting the heading currently in view.') }}</p>
</section>
</article>
<x-ui.table-of-contents target="#toc-demo" class="md:sticky md:top-4 md:self-start" />
</div>
</div>
Validation
Validation support: native. Keep the error message connected with aria-describedby.
<form wire:submit="save" class="space-y-2">
<brok:table-of-contents
wire:model="value"
:aria-invalid="$errors->has('value') ? 'true' : 'false'"
aria-describedby="value-error"
/>
@error('value')
<p id="value-error" role="alert">{{ $message }}</p>
@enderror
<brok:button type="submit" wire:loading.attr="disabled">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving…</span>
</brok:button>
</form>
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'target' => null,
'selector' => 'h2, h3',
'items' => [],
'title' => null,
// Accessible name of the <nav>.
'label' => null,
// list (default): plain rows, the active one in foreground weight.
// rail: a hairline down the start edge with the active row marked on it,
// and a small uppercase title — the "On this page" rail of a docs page.
'variant' => 'list',
// The sections' scroll container: null observes the viewport, a selector
// names the element, 'auto' finds the nearest scrollable ancestor of the
// first heading (a list inside a scrolling panel).
'root' => null,
// Pixels trimmed from the top of the active band, for a fixed header.
'offset' => 0,
])
@php
// Accept a string, a backed enum or a Stringable for `variant`.
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$variant = $styles['normalizeVariant']($variant);
@endphp
{{--
Table of Contents (spec §16B).
A navigation list of in-page headings. Headings are either scanned from the
`target` container at runtime or passed explicitly via `:items`. Explicit
items are rendered on the server, so the links exist (and deep links resolve)
before Alpine boots; scanned items render client-side. An item may carry a
`meta` string (a date, a count) shown after its label. The active section is
tracked with an IntersectionObserver (the last section wins once the page is
scrolled to its end) and reflected with `aria-current="location"`; links
smooth-scroll (motion-reduce safe), update the URL hash and hand focus to
the heading so the next Tab continues into the section.
--}}
@php
$variant = in_array($variant, ['list', 'rail'], true) ? $variant : 'list';
$rail = $variant === 'rail';
$indent = $rail
? static fn (int $level): string => $level <= 2 ? 'ps-3' : ($level === 3 ? 'ps-6' : 'ps-9')
: static fn (int $level): string => $level <= 2 ? 'ps-0' : ($level === 3 ? 'ps-4' : 'ps-8');
$indentBinding = $rail
? "{ 'ps-3': item.level <= 2, 'ps-6': item.level === 3, 'ps-9': item.level >= 4 }"
: "{ 'ps-0': item.level <= 2, 'ps-4': item.level === 3, 'ps-8': item.level >= 4 }";
$ring = 'focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring focus-visible:ring-offset-[length:var(--ring-offset-width)] focus-visible:ring-offset-background';
$linkClass = $rail
? '-ms-px flex items-baseline justify-between gap-2 border-s border-transparent py-1 pe-2 text-sm transition-colors hover:border-foreground/40 hover:text-foreground motion-reduce:transition-none '.$ring
: 'flex items-baseline justify-between gap-2 py-1 text-sm transition-colors hover:text-foreground motion-reduce:transition-none '.$ring;
$activeClass = $rail ? 'border-foreground font-medium text-foreground' : 'font-medium text-foreground';
$idleClass = $rail ? 'border-transparent text-muted-foreground' : 'text-muted-foreground';
$titleClass = $rail
? 'mb-2 px-3 text-2xs font-semibold uppercase tracking-wider text-muted-foreground'
: 'mb-2 text-sm font-medium text-foreground';
$listClass = $rail ? 'space-y-0.5 border-s border-border' : 'space-y-1';
// scroll-spy renders this component under its own name; the browser
// keeps only the first of two identical attributes, so read the override
// out of the bag and print exactly one.
$slotName = (string) ($attributes->get('data-slot') ?: 'table-of-contents');
@endphp
<nav
data-slot="{{ $slotName }}"
data-variant="{{ $variant }}"
aria-label="{{ $label ?? __('Table of contents') }}"
x-data="uiTableOfContents({ target: @js($target), selector: @js($selector), items: @js($items), root: @js($root), offset: @js((int) $offset) })"
{{ $attributes->except('data-slot')->merge(['class' => 'text-sm text-muted-foreground']) }}
>
@if ($title !== false)
<p data-slot="table-of-contents-title" class="{{ $titleClass }}">
{{ $title ?? __('On this page') }}
</p>
@endif
<ol data-slot="table-of-contents-list" class="{{ $listClass }}">
@if ($items !== [])
@foreach ($items as $item)
<li data-slot="table-of-contents-item" @if (! $rail) class="{{ $indent((int) ($item['level'] ?? 2)) }}" @endif>
<a
href="#{{ $item['id'] }}"
@click.prevent="scrollTo(@js($item['id']))"
:aria-current="active === @js($item['id']) ? 'location' : null"
:class="active === @js($item['id']) ? @js($activeClass) : @js($idleClass)"
class="{{ $linkClass }} {{ $rail ? $indent((int) ($item['level'] ?? 2)) : '' }}"
>
<span class="min-w-0 truncate">{{ $item['label'] }}</span>
@if (! empty($item['meta']))
<span class="shrink-0 font-mono text-xs">{{ $item['meta'] }}</span>
@endif
</a>
</li>
@endforeach
@else
<template x-for="item in items" :key="item.id">
<li data-slot="table-of-contents-item" @if (! $rail) :class="{{ $indentBinding }}" @endif>
<a
:href="'#' + item.id"
@click.prevent="scrollTo(item.id)"
:aria-current="active === item.id ? 'location' : null"
:class="[active === item.id ? @js($activeClass) : @js($idleClass), {{ $rail ? $indentBinding : '{}' }}]"
class="{{ $linkClass }}"
>
<span class="min-w-0 truncate" x-text="item.label"></span>
<span class="shrink-0 font-mono text-xs" x-show="item.meta" x-text="item.meta"></span>
</a>
</li>
</template>
@endif
</ol>
</nav>
/**
* Table of Contents behavior (spec §16B).
*
* Builds a navigation list from the headings inside a `target` container (or
* the whole document) matching `selector`, unless an explicit `items` array is
* supplied. Each scanned heading is given a stable id (slugified from its text)
* when it lacks one. An IntersectionObserver highlights the topmost heading in
* view as `active`. The observer runs against the viewport by default; `root`
* names the sections' scroll container instead (a selector, or 'auto' for the
* nearest scrollable ancestor of the first heading) so a list inside a
* scrolling panel tracks that panel, and `offset` (px) trims the top of the
* active band for a fixed header. The end of the scroll area marks the last
* heading (a short last section never reaches the band). `scrollTo`
* smooth-scrolls to a section, honoring prefers-reduced-motion. Listeners are
* torn down on destroy.
*/
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiTableOfContents', (config = {}) => ({
items: config.items ?? [],
active: null,
target: config.target ?? null,
selector: config.selector ?? 'h2, h3',
root: config.root ?? null,
offset: config.offset ?? 0,
_observer: null,
_headings: [],
_scroller: null,
_onScroll: null,
init() {
const root = this.target ? document.querySelector(this.target) : document;
if (!root) {
return;
}
// Seed items from the DOM only when none were provided explicitly.
if (!this.items.length) {
const headings = Array.from(root.querySelectorAll(this.selector));
this.items = headings.map((el) => {
if (!el.id) {
// Disambiguate duplicate headings: append -2, -3, … so
// getElementById later resolves the right one, not the first.
const base = this.slugify(el.textContent || '');
let id = base;
let n = 2;
while (document.getElementById(id)) {
id = `${base}-${n++}`;
}
el.id = id;
}
return {
id: el.id,
label: (el.textContent || '').trim(),
level: Number(el.tagName.replace(/\D/g, '')) || 2,
};
});
}
// Resolve the heading elements we will observe (works for both
// scanned and explicit items, as long as the ids exist in the DOM).
this._headings = this.items
.map((item) => document.getElementById(item.id))
.filter(Boolean);
if (!this._headings.length) {
return;
}
const rootEl = this.resolveRoot(this._headings[0]);
this._observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
if (visible.length) {
this.active = visible[0].target.id;
}
},
{ root: rootEl, rootMargin: `-${this.offset}px 0px -70% 0px`, threshold: 0 }
);
this._headings.forEach((el) => this._observer.observe(el));
// A short last section never reaches the observer's top band, so the
// end of the scroll area marks the last heading instead.
this._scroller = rootEl ?? window;
this._onScroll = () => {
const el = rootEl ?? document.documentElement;
const top = rootEl ? rootEl.scrollTop : window.scrollY;
const height = rootEl ? rootEl.clientHeight : window.innerHeight;
if (top + height >= el.scrollHeight - 2) {
this.active = this._headings[this._headings.length - 1].id;
}
};
this._scroller.addEventListener('scroll', this._onScroll, { passive: true });
// Default to the hash, else the first heading, until the observer reports.
const hash = decodeURIComponent(location.hash.slice(1));
if (this.active === null && this.items.length) {
this.active = this.items.some((item) => item.id === hash) ? hash : this.items[0].id;
}
},
/**
* The observer's root: null for the viewport (default), the element a
* selector names, or — for 'auto' — the nearest ancestor of the first
* heading that scrolls vertically. Judged by overflow alone: the
* component may initialise while its stage is still hidden (a docs
* tab, a collapsed panel), when scrollHeight and clientHeight are 0.
*/
resolveRoot(first) {
if (!this.root) {
return null;
}
if (this.root !== 'auto') {
return document.querySelector(this.root);
}
for (let node = first?.parentElement; node && node !== document.body; node = node.parentElement) {
const { overflowY } = getComputedStyle(node);
if (overflowY === 'auto' || overflowY === 'scroll') {
return node;
}
}
return null;
},
scrollTo(id) {
const el = document.getElementById(id);
if (!el) {
return;
}
// A CSS `scroll-behavior: auto` under reduced motion does not override
// a JS scrollIntoView({ behavior: 'smooth' }) argument.
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
el.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' });
this.active = id;
history.replaceState(null, '', `#${id}`);
// The prevented click cancelled the browser's focus move too: put focus
// on the heading so the next Tab continues into the section.
if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1');
el.focus({ preventScroll: true });
},
slugify(text) {
return text
.toString()
.trim()
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
},
destroy() {
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
if (this._scroller && this._onScroll) {
this._scroller.removeEventListener('scroll', this._onScroll);
this._onScroll = null;
this._scroller = null;
}
},
}));
});
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.3.2 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0