SlidingPill
Hero example
When to use
Reach for SlidingPill when you need a horizontal switcher between two to five related views and the visual identity is "tabs with a sliding indicator" — the canonical mounts are the lifecycle-stage switcher on entity pages (Draft / Active / Dissolving), the timescale switcher in the Compliance view (Week / Month / Quarter), and the agent-tier switcher in the developer playground (tier_1 / tier_2 / tier_3 / tier_4). The animated indicator is the visual identity; if you don't need the animation, use a plain button group.
Don't reach for SlidingPill for more than ~5 items — the indicator track grows past the readable width and the slide animation reads as restless. For 6+ tabs use a <Tabs> primitive. Don't use it for navigation between routes either — the indicator implies a single-surface switch, not a page change. If you need route navigation, use a navigation pill with <Link> items.
Anatomy
Three structural parts:
- Track. Outer
<div class="m-sliding-pill">. Carries the size modifier class (--sm/--md/--lg). - Indicator. Absolute-positioned
<span aria-hidden>that reads itstransform: translateX(…)andwidthfrom the active item's measured rect. Hidden (opacity 0) when no item is active. - Items. One
<button class="m-sliding-pill__item">perSlidingPillItem. Each carriesdata-id="{id}"anddata-active="true|false". The active item's foreground is brighter; inactive items are muted.
The slide math: on every active change, useLayoutEffect measures the active button's offsetLeft and offsetWidth, then sets the indicator's translation and width. The CSS recipe handles the transition.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| itemsrequired | SlidingPillItem[] | — | — |
| activerequired | string | — | — |
| onChangerequired | (id: string) => void | — | — |
| size | "sm" | "md" | "lg" | "md" | — |
| className | string | — | — |
size defaults to "md". Use "sm" for dense surfaces (Composer chip row), "lg" for hero-tier switchers (entity-page header).
States
| State | Trigger | Visual |
|---|---|---|
| Default (no active) | active doesn't match any item id | Indicator hidden (opacity 0); all items muted |
| Active item set | active matches an item id | Indicator slides to that item; item foreground brightens |
| Hover (inactive item) | Mouse hover | Item foreground brightens slightly |
| Focus-visible | Keyboard focus | Peach ring on the focused item |
| Empty | items: [] | Empty track renders without crashing — defensive against data-loading races |
Density
The size prop controls visual density; the site-level density toggle has no effect on SlidingPill.
size | Height | Use |
|---|---|---|
sm | 24px | Composer chip row, dense filter bars |
md (default) | 32px | Entity-page lifecycle switcher, in-card view-mode pills |
lg | 40px | Marketing hero, full-page tab switchers |
Themes
In dark, the track background drops to a semi-transparent ink fill; the indicator uses the canonical glass surface. In forced-colors, the indicator collapses to Highlight and active-item foreground to HighlightText.
Tokens consumed
Track background reads --ink-4 / --ink-6 on hover. Indicator background reads --surface-glass (or --surface-card if glass isn't available). Active foreground reads --fg; inactive reads --fg-muted. Border radius reads --radius-pill.
Accessibility
- Keyboard interactions. Each item is a real
<button>. Tab moves between items; Enter and Space activate. Arrow keys do not move focus — Tab is the contract. - ARIA roles and properties. Default to implicit
buttonrole. If you need radio-group semantics, wrap theSlidingPillinrole="tablist"and passrole="tab"plusaria-selectedvia custom item rendering at the consumer layer (the current API doesn't propagate role). - Focus order. DOM order. The active item is not skipped — Tab still lands on it.
- Screen-reader expectations. Each item announces as "
{label}, button." The active state must be conveyed to assistive tech by the consumer (passaria-pressedoraria-selectedif used as a toggle group). - Reduced motion. Indicator slide transition collapses to instant under
prefers-reduced-motion: reduce. The indicator still moves to the correct rect; it just doesn't animate the path. - Forced colors. Indicator →
Highlight; active foreground →HighlightText; track →CanvaswithButtonBorder. - WIG rules. Real
<button>elements (semantic-element rule).touch-action: manipulationon items prevents 300ms double-tap delay.:focus-visiblering (not:focus) per the focus rule. Indicator isaria-hidden(decorative).
Do / Don't
items between 2 and 5. The track is tuned to that range.?view=week) so the choice deep-links and survives refresh (WIG navigation rule).display_name). Don't duplicate the canonical labels.Recipes
This component appears in:
- Equity recipe — the timescale switcher above the cap-table donut.
- Docs corpus recipe — the view-mode switcher (Tree / List / Graph) on the corpus explorer.
Code example
import { SlidingPill } from "@matter/components";
import { useState } from "react";
export function TimescaleSwitcher() {
const [view, setView] = useState("month");
return (
<SlidingPill
items={[
{ id: "week", label: "Week" },
{ id: "month", label: "Month" },
{ id: "quarter", label: "Quarter" },
{ id: "year", label: "Year" },
]}
active={view}
onChange={setView}
size="md"
/>
);
}Source
packages/components/src/SlidingPillInlineToolCall
Pill-style tool-call indicator for the assistant chat history. Single line: status dot + METHOD + path + status label + latency. Use whenever an agent invokes a Matter API tool and the user should see what call happened.
ThinkingTree
Quiet editorial reasoning surface — a flat list of sequential steps with a five-state vocabulary (done · active · queued · failed · skipped), a hairline rail that darkens to the success ramp as work completes, and a translucent glass pill carrying the active row's continuous text shimmer.