> ## Oymyakon DS index
> Fetch the full index at: https://super-dollop-pzmo65r.pages.github.io/llms.txt
> HTML preview: https://super-dollop-pzmo65r.pages.github.io/sliding-button.html
> Source: specs/components/sliding-button/sliding-button.md, specs/components/sliding-button/sliding-button-a11y.md, specs/components/sliding-button/capabilities.json

---

# SlidingButton · Oymyakon DS 3

> A swipe-to-confirm action control: a horizontal track with a draggable Slider that a person pulls
> from one edge to the other to commit, resolving into a Loading → Success or Error result.

**Version:** 3.0.8 · **Status:** Draft · **Figma:** `[SlidingButton] 3.0` — node `27954:1393` · spec frame `28671:17340` (Oymyakon 3.28.3)

---

## 1. Description

SlidingButton asks for a deliberate gesture rather than a single tap. Where [Button](https://super-dollop-pzmo65r.pages.github.io/button.md)
commits a person on contact, SlidingButton requires them to drag the Slider the full width of the
track, so it fits an action whose cost of being triggered by accident is high — confirming a payment,
starting or ending a ride, sending an SOS, deleting an account.

The gesture is the whole point: the Slider follows the finger, the label recolours as the Slider
passes over it, and only a completed drag arms the action. Completion hands off to an asynchronous
result — the component shows its own Loading state while the request is in flight, then resolves to
Success or Error without the caller swapping it for another control.

A control that commits on a single tap is Button; a control whose whole content is an icon is Icon
Button. SlidingButton is the one action worth a full gesture.

---

## 2. Anatomy

The Slider is the moving part, built on the **CustomSquircle** base; the Background is the track it
moves along; the **StartText** block (Title + Subtitle) fills the track width, centred under the
Slider's path.

```
SlidingButton              .sliding-button              horizontal track, radius s20 — required
├── Background             .sliding-button__background       the track fill (component background) — required
├── Slider                 .squircle .sliding-button__slider CustomSquircle chip, pinned to the start edge — required
│   └── StartSlot          .sliding-button__start-slot       swap slot, default forward-arrow-right icon — required
├── StartText              .sliding-button__text             fills the width — Title + Subtitle — required
│   ├── Title              .sliding-button__title            centred label — required
│   └── Subtitle           .sliding-button__subtitle         centred second line — optional
├── Loader                 .sliding-button__loader           result-cycle spinner (Loader L) — shown in Loading
├── Check                  .sliding-button__check            Success glyph — shown in Success
├── Close                  .sliding-button__close            Error glyph — shown in Error
└── Skeleton               .skeleton .sliding-button__skeleton  hosted Skeleton — shown in Skeleton
```

| Element | Class | Required | Notes |
|---|---|---|---|
| Root | `.sliding-button` | required | The horizontal track and the focusable, interactive element. Radius `s20`; reserves `s68` horizontal padding for the label on each side |
| Background | `.sliding-button__background` | required | The track fill behind everything. Default `Background/InversePrimary` |
| Slider | `.squircle` `.sliding-button__slider` | required | The draggable chip, built on the **CustomSquircle** base (`.squircle`) in every state — the base carries the box model (radius `s16`, flex-centre, overflow, box-sizing). `s56 × s56` at rest, resting at the start edge, inset `s4` from the track. Its fill is **Slider Background**, default `Background/Brand`. Its width changes only on tap and on drag (never reaching the right edge — `s4` clear); its icon rides the leading edge |
| StartSlot | `.sliding-button__start-slot` | required | The icon inside the Slider. Swap slot, default `forward-arrow-right` from `icons/outlined/forward-arrow-right`. On `Background/Brand` the glyph is `TextAndIcon/AlwaysDark` |
| StartText | `.sliding-button__text` | required | The Title + Subtitle block. **Fills** the track width — the Track reserves `s68` on each side; StartText's own horizontal padding is `s0`, vertical padding `s4` (keeps an `s4` inset from the track top/bottom). `s2` gap between the two lines; each line single-line, centred and truncated. Sits above the Slider (z3) |
| Title | `.sliding-button__title` | required | Centred label inside StartText, **above the Slider** so it stays visible as the Slider passes over it. Recolours to `TextAndIcon/AlwaysDark` once the Slider reaches 50% (§5). Hidden in Loading, Success and Error (the result glyph takes the track) |
| Subtitle | `.sliding-button__subtitle` | optional | Second line under the Title inside StartText, same layering and recolour behaviour |
| Loader | `.sliding-button__loader` | required | The result-cycle spinner (z4), Loader size `L` (`s32` host), `TextAndIcon/AlwaysDark` on the brand fill; `opacity 0` except in Loading (§5) |
| Check | `.sliding-button__check` | required | The Success glyph (`check`, `s32`, `TextAndIcon/AlwaysDark`) centred over the brand fill; shown only in Success |
| Close | `.sliding-button__close` | required | The Error glyph (`close`, `s32`, `TextAndIcon/AlwaysLight`) centred over the `Accent/Red1` fill; shown only in Error |
| Skeleton | `.skeleton` `.sliding-button__skeleton` | required | A hosted [Skeleton](https://super-dollop-pzmo65r.pages.github.io/skeleton.md) instance filling the track shape (radius `s20`), shown only in the Skeleton state — the Slider, label, icon, result glyphs and track fill are removed behind it |

The Slider is the only part a person touches; the StartText block is passive, and the StartSlot
icon travels with the Slider rather than being a target of its own. RTL mirrors the whole layout: the
Slider rests at the right edge and its default icon flips to point left (see § 3, § 5).

---

## 3. Variants and sizes

| Parameter | Values |
|---|---|
| State | `Standard` · `On tap` · `On drag` · `Loading` · `Success` · `Error` · `Disabled` · `Skeleton` (default `Standard`) — see § 4 |
| RTL | `Off` · `On` (default `Off`) — mirrors the Slider to the end edge and flips the default icon |
| Subtitle | boolean — on by default; a single-line Title is available with it off |
| Title style | `Heading 4` (default) · `Heading 3` · `Promo Body` — the Title text style, a custom-base override (§7) |
| Subtitle style | `Compact Body` (default) · `Main Body` — the Subtitle text style, a custom-base override (§7) |
| StartSlot content | `INSTANCE_SWAP` — any icon; default `forward-arrow-right` |

SlidingButton ships as a single size. The track hugs its container's width — a swipe needs a
travel distance to read as a swipe, so unlike Button it does not hug its label. Height, radius and
the label styles are fixed (§ 7, § 8).

Theme is not an axis: Light and Dark resolve by token name from the one set of colour tokens in § 6.

> **Figma note:** the source frame also sketches a reduced text-only / icon-only composition beside
> the main track. It is not yet a contracted variant of this component and is out of scope for 3.0.0;
> if it is promoted it becomes a `Type` axis in a later version.

---

## 4. States

The rest state is **Standard**. The list is the component's full lifecycle; the interaction states
run in sequence, and the result states are entered by the host resolving the armed action.

| State | Description |
|---|---|
| Standard | Rest. The Slider sits at the start edge; Title and Subtitle read on the `Background/InversePrimary` track |
| On tap | The Slider is touched: its leading inner padding grows to `s32`, which widens the Slider by the same delta (the button grows, not only the icon), and a haptic fires (§5) — iOS Light Impact, Android none |
| On drag | The Slider follows the finger, stretching toward the drag direction, and its icon rides the leading edge so it travels in the drag direction. From **30%** of the track width the Background morphs from `Background/InversePrimary` toward `Background/Brand`, completing at 80%. Past **50%** the Slider covers the label, and Title / Subtitle recolour to `TextAndIcon/AlwaysDark`. Releasing at **80%** or more enters `Loading`; a release below 80% or a reverse drag returns the Background and springs the Slider back to `Standard` |
| Loading | Entered when the drag passes the 80% auto-completion threshold. The `forward-arrow-right` icon fades out and the Title / Subtitle give way to a [Loader](#5-animation-and-behavior) at size `L` (`s32`). The armed action is now in flight |
| Success | The request resolved successfully. The Loader is replaced by a `check` glyph and the Slider Background expands from the centre to fill the whole track |
| Error | The request failed. The Slider Background and the track turn `Accent/Red1`, and the Loader gives way to a single `close` glyph (`s32`, `TextAndIcon/AlwaysLight`) centred over the fill — the Title, Subtitle and start-slot icon stay hidden, as in Success. The Slider then retracts to the start edge as its fill returns to `Background/Brand`, leaving the component back at Standard for another attempt |
| Disabled | Non-interactive. Fill collapses to `Surface/OnWhite` and the label and icon to `TextAndIcon/Disabled`, the same token-swap contract Button uses; no gesture is accepted |
| Skeleton | Placeholder while content resolves: a hosted [Skeleton](https://super-dollop-pzmo65r.pages.github.io/skeleton.md) instance (`.skeleton .sliding-button__skeleton`) fills the track shape (radius `s20`); the Slider, label, icon, result glyphs and track fill are removed behind it |

`On tap` and `On drag` are the interaction phases that precede the result cycle — a touch, then the
drag itself. Figma groups the three enabled states under `Enabled` (`Rest` / `On tap` / `On drag`) in
a frame titled *States, Behavior and Animation*; the rest state keeps the DS-canonical name
**Standard**. The drag thresholds are detailed in § 5.

`Loading`, `Success` and `Error` are a result cycle, not user-selectable variants: `Loading` follows
a drag past the 80% threshold, and it resolves to exactly one of `Success` or `Error`. `Success` is
terminal; `Error` returns the component to `Standard`. `Disabled` is expressed by a token swap rather
than `opacity`, which keeps the label's contrast predictable on every surface.

---

## 5. Animation and behavior

Motion follows [`motion-rules.md`](https://super-dollop-pzmo65r.pages.github.io/motion.md). No easing or duration is
hardcoded; every transition names a DS motion token. The rows mirror the Figma *Animation* frame,
which documents the transitions **between states** rather than per-property events.

| Transition | Token | CSS |
|---|---|---|
| **Default → On Tap → Default** — Slider leading padding → `s32` | Enter `--transforming-state-fast`, Exit `--pattern-scale-short-hide` | `padding-inline-start`; a haptic fires alongside — iOS Light Impact, Android none |
| **On tap → Default** — the Slider tracks the pointer | `--curve-linear` (Linear) | direct manipulation — width / `transform` follow the finger 1:1, no timed token. Past 50% the label recolours (`color` → `var(--text-and-icon-always-dark)`) and the `Background/Brand` morph layer crossfades, both on `--pattern-color` |
| **On Drag → Default** — release below 80% returns to Standard | `--pattern-scale-medium-appear` (Scale Medium) | `width` / `left` settle back to rest; a release at ≥80% instead enters `Loading` |
| **Loading → Success** — the Loader gives way to the check | `--component-fade-m-appear` (fadeM) | `opacity` + `transform: scale` (Loader ⇄ result); the spinner rotates on `--pattern-spin`; iOS Success haptic |
| **Loading → Error** — the Loader gives way to the `close` glyph, the fill turns red | `--component-fade-m-appear` (fadeM) | `opacity` + `transform: scale` (Loader → close), `background-color` → `var(--accent-red1)`; iOS Error haptic |
| **Error → Default** — the control returns for another attempt | `--component-fade-m-hide` (fadeM) | `opacity` + `background-color` back to `Background/Brand` / `InversePrimary`, alongside the Slider retract |

The track fill snaps to `Background/Brand` on entering `Loading` with **no transition**, so the `s4`
border never flashes the dark `InversePrimary` on the way in.

```css
.sliding-button__slider {
  transition: padding-inline-start var(--transforming-state-fast),
              background-color var(--pattern-color);
}
.sliding-button__title,
.sliding-button__subtitle {
  transition: color var(--pattern-color);
}
.sliding-button__loader,
.sliding-button__check,
.sliding-button__start-slot {
  transition: opacity var(--component-fade-m-appear);
}
```

Reduced motion is handled at the token in `motion.css` (every `--transforming-*`, `--pattern-*` and
`--component-*` drops to `0ms` and the loop patterns to `none`), so the component carries no override
of its own.

The result swaps and colour changes ride `opacity`, `transform`, `background-color` and `color`; the
Slider's size and position settle (`width`, `left`, `padding-inline-start`) are layout properties,
transitioned only in the short state changes and the retract — never during finger-tracking, where the
Slider is bound to the pointer on a Linear curve and carries no timed token. Two thresholds govern the
drag: at **50%** of the track
width the label recolours as the Slider passes over it, and at **80%** the gesture completes on
release — a release at 80% or more enters `Loading`, a release below 80% returns to `Standard` on
Scale Medium. Reduced motion zeroes every token at the source, so the result cycle still happens but
as instant swaps and the spinner resolves to a static glyph.

**Haptic feedback (iOS).** Three transitions fire a haptic, each **once** at the moment the state is
entered: **On tap** → Light Impact, **Success** → Success, **Error** → Error. Android plays no
haptic.

The spinner is an instance of **Loader** at size `L` (`s32` host), the same relationship
[Button](https://super-dollop-pzmo65r.pages.github.io/button.md#5-animation-and-behavior) has to it. Loader has no Markdown spec or
`shared.css` build in this repo yet, so — as Button does — SlidingButton reproduces the arc geometry
inline and consumes `Patterns/Spin` until Loader ships its own spec.

---

## 6. Color tokens

One token name per role; Light / Dark substitution happens by name.

| Element | Token | Role |
|---|---|---|
| Background (track) — rest | `var(--background-inverse-primary)` | The track fill — flips against the theme |
| Background (track) — under drag | `var(--background-brand)` | Morphs in from 30% → 80% of the drag; returns to `InversePrimary` on release / reverse drag |
| Slider Background | `var(--background-brand)` | The Slider fill at rest |
| StartSlot icon | `var(--text-and-icon-always-dark)` | The glyph on the brand-green Slider |
| Title / Subtitle — Standard | `var(--text-and-icon-inverse-primary)` | Reads on the `InversePrimary` track |
| Title / Subtitle — under the Slider | `var(--text-and-icon-always-dark)` | While the Slider overlaps the label (§5) |
| Slider Background — Error | `var(--accent-red1)` | The failed-attempt fill, before returning to brand |
| Error glyph (`close`) | `var(--text-and-icon-always-light)` | The white close centred on the red fill |
| Slider Background — Success | `var(--background-brand)` | The green fill expands to cover the whole track |

| Element | Token — Disabled |
|---|---|
| Track fill | `var(--surface-on-white)` |
| Slider fill | `var(--surface-on-grey)` |
| Label and icon | `var(--text-and-icon-disabled)` |

The Title and Subtitle pair with the track: `TextAndIcon/InversePrimary` on `Background/InversePrimary`
holds its contrast in both themes. Once the Slider passes over the label the reference surface becomes
the brand-green Slider, so the label crosses to `TextAndIcon/AlwaysDark` for the duration of the
overlap. `Brand` appears only as the Slider fill, never behind the resting label.

---

## 7. Typography

| Element | Style (default) | Custom-base options |
|---|---|---|
| Title | Heading 4 — Suisse Intl Semibold 17 / 20 | Heading 3 (`.sliding-button--title-heading-3`) · Promo Body (`.sliding-button--title-promo-body`) |
| Subtitle | Compact Body — Suisse Intl Book 14 / 16 | Main Body (`.sliding-button--subtitle-main-body`) |

The Title and Subtitle text styles are **replaceable defaults** (a custom-base override, §3): the
Title may take **Heading 4** (default), **Heading 3** or **Promo Body**, and the Subtitle **Compact
Body** (default) or **Main Body**. Each is a root modifier that resets the label's `var(--text-*)`
group (family + size + weight + line-height together); the default needs no class. The tokens for all
five styles live in `typography.css`.

The Title carries the action's name and the Subtitle an optional qualifier, the same two-row pattern
and styles as [Button](https://super-dollop-pzmo65r.pages.github.io/button.md#7-typography) `L`. An implementation sets family, size,
weight and line-height together from `var(--text-{group}-{name}-*)` rather than from the numbers here.
Scaling follows the style (`full`).

Each of Title and Subtitle is a **single line** within the label zone — the track width minus the
`s68` reserve on each side (§8). Text that exceeds the zone truncates with an ellipsis
(`white-space: nowrap; overflow: hidden; text-overflow: ellipsis`, `max-width: 100%`); the label never
wraps to a second line or pushes the track wider.

> **Figma note:** the Title / Subtitle styles are aligned to the Button `L` label pair; confirm the
> exact text tokens against `[SlidingButton] 3.0` when the Figma variables are read directly.

---

## 8. Spacing

| Property | Token | Value |
|---|---|---|
| Track height | `var(--sp-s64)` | 64 |
| Track horizontal padding — reserved for the label, each side | `var(--sp-s68)` | 68 |
| Radius — track | `var(--sp-s20)` | 20 |
| Radius — Slider | `var(--sp-s16)` | 16 |
| Slider initial width — Standard | `var(--sp-s56)` | 56 |
| Slider height | `var(--sp-s56)` | 56 |
| Slider inset from the track edges — all sides | `var(--sp-s4)` | 4 |
| Slider leading inner padding — Standard | `var(--sp-s16)` | 16 |
| Slider leading inner padding — On tap | `var(--sp-s32)` | 32 |
| StartSlot icon (`forward-arrow-right`) size | `var(--sp-s24)` | 24 |
| Success glyph (`check`) size | `var(--sp-s32)` | 32 |
| Error glyph (`close`) size | `var(--sp-s32)` | 32 |
| StartText horizontal padding | `var(--sp-s0)` | 0 |
| StartText vertical padding | `var(--sp-s4)` | 4 |
| Title / Subtitle gap | `var(--sp-s2)` | 2 |

The Slider is built on the CustomSquircle base (`.squircle`) and is a fixed `s56 × s56` chip at rest,
inset `s4` from all track edges (so the `s64` track leaves a `s4` border around it). Its width changes
only on tap and on drag — nothing else moves it. On tap the leading inner padding grows to `s32`,
which widens the Slider to `s72`; on drag the width follows the finger, up to a maximum that keeps the
Slider `s4` clear of the right edge. The StartText block fills the track width with the Track reserving
`s68` on each side (its own horizontal padding `s0`, vertical padding `s4` so it keeps an `s4` inset
from the track top/bottom and never butts the edges when it grows past one line); its two lines are
single-line and truncate. Every value moves with the SP scale modes (100% / 130% / 150%).

---

## 9. Usage context

**When to use**

- A high-consequence confirmation where an accidental tap would be costly — confirm a payment, start
  or end a ride, send an SOS, delete an account.
- A single, deliberate commitment that benefits from a gesture the hand cannot make by mistake.
- An action with an asynchronous result the control should show inline — the Loading → Success / Error
  cycle is part of the component.

**When not to use**

- An ordinary action that commits on contact — that is [Button](https://super-dollop-pzmo65r.pages.github.io/button.md).
- Navigation to another screen without committing to anything — that is a link, which carries an
  underline (see [`typography-rules.md`](https://super-dollop-pzmo65r.pages.github.io/typography.md)).
- An on/off setting — that is Switch.
- A control whose whole content is an icon — that is Icon Button.

A swipe costs more effort than a tap by design; spending it on a low-stakes action trains people to
swipe without reading. One SlidingButton per view keeps the gesture meaningful.

**Related components** — [Button](https://super-dollop-pzmo65r.pages.github.io/button.md) (the tap-to-commit sibling),
[IconContainer](https://github.com/inDriver/oymyakon-ds/blob/main/specs/primitives/icon-container.md) (the StartSlot glyph),
[Skeleton](https://super-dollop-pzmo65r.pages.github.io/skeleton.md) (the `Skeleton` state), Loader (the `Loading` spinner, no spec
yet), Switch.

---

## 10. Accessibility

Full spec: [`sliding-button-a11y.md`](https://super-dollop-pzmo65r.pages.github.io/sliding-button.md).

The root is the single interactive element and the single focus stop. A drag gesture is not reachable
by a keyboard or by a screen-reader user swiping to navigate, so the control also exposes a plain
activation path: activating the focused control arms the action the same way a completed drag does,
and the visible Slider is the enhancement over that baseline, not the only way in. Its accessible
name is the action alone — "Slide" is dropped, since a swipe navigates rather than drags when a
screen reader is on. The track is wide and its height clears the WCAG 2.5.5 target on its own.

The result cycle is announced rather than shown by colour alone: entering `Loading` exposes a busy
state, and the resolution to `Success` or `Error` is published to a live region so a non-sighted
person learns the outcome without watching the fill. Because the label is replaced while loading, the
accessible name is preserved separately so the control keeps its identity mid-request. `Disabled` is
announced as a disabled state, not signalled by dimming alone.

---

## 11. Analytics and coverage contract

Contract per [`prototype-analytics-and-coverage.md`](https://github.com/inDriver/oymyakon-ds/blob/main/docs/prototype-analytics-and-coverage.md).

| Field | Value |
|---|---|
| `data-ds-component` | `sliding-button` |
| `data-ds-variant` | `—` (single configuration; `rtl` when mirrored) |
| Coverage unit | yes |
| Tap target model | root — the whole control is the only target; the Slider is not a separate one |
| Actions | `slide` — emitted once, when the drag passes the 80% auto-completion threshold and arms the action |
| Internal targets | none |
| Emits value | no — `data-ds-state` reports the lifecycle, not a user value |

The completed drag is the one recorded action; the drag behaviors (`on-tap`, `on-drag`) are live
states, not taps, and the result cycle (`loading`, `success`, `error`) updates `data-ds-state`
without emitting an action of its own.

```html
<button class="sliding-button"
        data-ds-component="sliding-button"
        data-ds-component-id="checkout.confirm.pay"
        data-ds-state="standard"
        data-ds-action="slide"
        aria-label="Pay">
  <span class="sliding-button__background" aria-hidden="true"></span>
  <span class="squircle sliding-button__slider">
    <span class="sliding-button__start-slot" aria-hidden="true"><!-- forward-arrow-right --></span>
  </span>
  <span class="sliding-button__text">
    <span class="sliding-button__title">Pay</span>
    <span class="sliding-button__subtitle">$51.25</span>
  </span>
  <span class="sliding-button__loader" aria-hidden="true"><!-- Loader L spinner --></span>
  <span class="sliding-button__check" aria-hidden="true"><!-- check --></span>
  <span class="sliding-button__close" aria-hidden="true"><!-- close --></span>
</button>
```

`data-ds-state` follows the live state — `standard`, `on-tap`, `on-drag`, `loading`, `success`,
`error`, `disabled`, `skeleton` — and is updated together with the matching ARIA attribute, so the
analytics value and the accessible value stay in step.

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.0.8 | 2026-08-17 | **Typography choice axes (custom base).** Title style — `Heading 4` (default) · `Heading 3` · `Promo Body`; Subtitle style — `Compact Body` (default) · `Main Body`. Each is a root modifier that resets the label's `var(--text-*)` group (`.sliding-button--title-heading-3` / `--title-promo-body` / `--subtitle-main-body`); the default needs no class. §3 axes, §7 table + prose, and `capabilities.json` (`axes.titleStyle` / `axes.subtitleStyle`) updated; tokens for all five styles already in `typography.css`. |
| 3.0.7 | 2026-08-17 | **Skeleton state now hosts a Skeleton instance.** New `.sliding-button__skeleton` layer (base `.skeleton`, `data-ds-component="skeleton"`) mounted in the markup and shown only in Skeleton, filling the track shape (radius `s20`) — the Slider, label, icon, result glyphs and track fill are removed behind it (previously a bare `--surface-on-white` track with contents `display:none`). §2 anatomy (tree + table row) and §4 Skeleton updated; `capabilities.json` synced. Preview (non-versioned): States section rebuilt to `spec-column` + numbered pointer-heads per specimen; Overview specimens carry numbered pointer-heads (dot touching the specimen) instead of caption badges; removed a dead `.sb-anno` dashed rule. |
| 3.0.6 | 2026-08-17 | **Doc-sync — spec caught up to the shipped `shared.css` / markup (no behaviour change).** §2 anatomy now lists the three result-cycle layers `.sliding-button__loader` / `.sliding-button__check` / `.sliding-button__close` (tree + table), and the §11 example markup includes them plus the `.sliding-button__text` wrapper. §5 corrected: `width` / `left` / `padding-inline-start` are named as the (layout) properties the retract / state changes transition — the earlier "everything stays on the compositor" claim removed. §6 Disabled split into **Track fill** `var(--surface-on-white)` + **Slider fill** `var(--surface-on-grey)` (the Slider token was undocumented). §7 overflow contract spelled out (single line, `s68` label zone, ellipsis truncation). `sliding-button-a11y.md` contrast row fixed to `Accent/Red1 + TextAndIcon/AlwaysLight` (the Error `close` glyph; label hidden) and its version aligned to 3.0.6. `capabilities.json` Disabled constraint updated with the Slider `surface-on-grey` fill. |
| 3.0.5 | 2026-08-17 | **StartText vertical padding `s4`** added (`.sliding-button__text` `padding: s4 s0`) so the label keeps an `s4` inset from the Track top/bottom and no longer butts the track edges when it grows past one line. §2 anatomy, §8 layout table + prose, and `capabilities.json` updated. Preview Layout StartText specimen: bounds highlight squared (no radius). |
| 3.0.4 | 2026-08-14 | Preview **Layout** section rebuilt to mirror the Figma *Layout (Standard preset)* frame — six measurement groups (Track, Slider, StartText, Loading, Success, Error), each with its state specimens (Slider default / on-tap; StartText short / long-truncated / subtitle-off / long-without-truncate) and the exact per-group measurement list. **Loader size L** applied: the spinner host is now `s32` (all three result glyphs `check` / `close` / Loader are `s32`); §4 / §5 and `capabilities.json` updated M → L. |
| 3.0.3 | 2026-08-14 | Layout / markup synced to Figma 3.28.3. **StartText** — Title + Subtitle wrapped in a new `.sliding-button__text` block that **fills** the track width (the Track reserves `s68` horizontal padding on each side; StartText's own padding `s0`), `s2` gap, each line single-line and truncated. **Slider** now built on the **CustomSquircle** base (`.squircle`) in every state — the base carries the box model (radius `s16`, flex-centre, overflow, box-sizing), the modifier sizes / positions / drives it (`height:auto` so the `s4` insets set the height). Result glyphs sized to `s32` (`check` and `close`); start-slot icon `s24` documented. §2 anatomy (StartText row, Slider CustomSquircle), §8 (`Track s68` reserve, StartText `s0`, icon sizes) updated; `capabilities.json` synced. Loader size (Figma `L`) still pending the Loader component spec. |
| 3.0.2 | 2026-08-14 | Error state redesigned to Figma 3.28.3: the fill turns `Accent/Red1` and a single `close` glyph (`s32`, `TextAndIcon/AlwaysLight`, `icons/outlined/actions/close.svg`) is centred over it — the Title, Subtitle and start-slot icon stay hidden, as in Success (previously the label returned in `AlwaysDark` with the arrow at the leading edge). New `.sliding-button__close` element (z4); §4 Error, §6 (Error glyph `AlwaysLight`), §8 (`close` `s32`) and §2 (Title hidden in Error) updated; RTL Error now direction-neutral (centred glyph). `capabilities.json` synced (close anatomy, motion + return constraints). |
| 3.0.1 | 2026-08-14 | §5 Animation synced to the Figma *Animation* frame (Oymyakon 3.28.3, node `28671:17340`). Transitions renamed to the between-states model — **Default → On Tap → Default**, **On tap → Default**, **On Drag → Default**, **Loading → Success**, **Loading → Error**, **Error → Default** — and remapped to DS tokens: on-tap Enter `--transforming-state-fast` / Exit `--pattern-scale-short-hide`; the drag on `--curve-linear` (direct manipulation, no timed token); the on-drag return on `--pattern-scale-medium-appear` (Scale Medium); the result swaps and the error tint / return on `--component-fade-m-appear` / `--component-fade-m-hide` (fadeM). Retired from §5: `--transforming-state-default`, `--transforming-state-slow`, `--pattern-scale-short-appear`, `--pattern-position-short-hide`. The 30% background-morph threshold is dropped (Figma names only 50% / 80%); haptics folded per transition. `shared.css` retimed to match; preview §5 titles + params updated with the two Success / Error demos kept separate. (Error close-icon `AlwaysLight` visual, Layout `Track s68` / Loader `L` land in a following change.) |
| 3.0.0 | 2026-08-05 | Initial spec, aligned to Figma `[SlidingButton] 3.0` (node `27954:1393`, frame *States, Behavior and Animation*). Six elements — Background, Slider, StartSlot (`forward-arrow-right`), Title, Subtitle — on a hug-width track. Full lifecycle states: Standard, On tap, On drag, Loading, Success, Error, Disabled, Skeleton (Figma groups the first three under `Enabled` as `Rest / On tap / On drag`; the rest state keeps the DS-canonical name Standard). Drag thresholds: the Background morphs `InversePrimary` → `Background/Brand` from 30% to 80% of the track width (returning on release / reverse drag), the label recolours to `AlwaysDark` past 50%, and releasing at ≥80% enters Loading while a release below 80% returns to Standard. Title / Subtitle render above the Slider (z3) — visible through the drag and in Error (`AlwaysDark`), hidden only in Loading / Success. Slider radius `s16` (track `s20`), inset `s4` from the track edges; on tap the padding step widens the Slider (the button grows, not only the icon); the icon rides the leading edge while dragging. Motion mapped to DS tokens: `--pattern-color` (drag / error tint), `--transforming-state-default` (opacity swaps), `--pattern-scale-short-appear` (Success fill from centre), `--pattern-position-short-hide` (Error return and sub-80% return), `--pattern-spin` (Loader M, inlined like Button). Colours: `Background/InversePrimary` track, `Background/Brand` Slider, `Accent/Red1` on Error, `TextAndIcon/InversePrimary` label crossing to `AlwaysDark` under the Slider. Web build shipped with it: `shared.css` classes (`.sliding-button` + layers + state modifiers), preview page `src/sliding-button.njk` (Cover + 6 sections + live drag demo), `capabilities.json` + registry row. A direct Figma measurement pass (exact Title/Subtitle type tokens, track insets) and a real-Chrome pointer-alignment pass remain. |

---

# SlidingButton — Accessibility

**Component:** SlidingButton
**Version:** 3.0.6
**Spec:** [`sliding-button.md`](https://super-dollop-pzmo65r.pages.github.io/sliding-button.md)

---

## Screen readers

Sliding buttons require a degree of interaction precision, which is not the best experience for users
on a screen reader. For a smoother experience, expose the control and let it be activated the way a
normal button is — a double-tap (or `Enter` / `Space`), not a swipe.

**Omit words like "Slide" from the accessible name.** With VoiceOver or TalkBack on, swipe gestures
are reserved for navigation and do something different, so a name like "Slide to …" both misleads and
describes a gesture the user will not perform. The name is the action alone — e.g. "Start ride".

The visible Slider, the 50% recolour and the 80% auto-completion threshold
(see [`sliding-button.md`](https://super-dollop-pzmo65r.pages.github.io/sliding-button.md) § 5) are pointer mechanics with no assistive-technology
equivalent and are not exposed; the control announces and behaves as a plain button.

---

## Role and ARIA

| Attribute | Value | Where |
|---|---|---|
| `role` | `"button"` | Native on `<button>`; a custom element sets it explicitly. **Not `"slider"`** — the control commits one action and has no adjustable value to report |
| `aria-label` | The action alone — e.g. "Start ride" (no "Slide") | The root; carries the action, and stays set while `Loading` replaces the label with the Loader |
| `aria-busy` | `"true"` | The root, in the `Loading` state |
| `aria-disabled` | `"true"` | The root, in the `Disabled` state |
| `aria-live` | `"polite"` | A status region (or the root) that speaks the `Success` / `Error` outcome — the result is not signalled by the fill colour alone |
| `aria-hidden` | `"true"` | The StartSlot arrow icon — decorative, the accessible name already carries the action |

A native `<button>` carries the role, the focus behaviour and both activation keys on its own, which
is why it is the DS markup. A `<div>` or `<span>` standing in for it needs `role="button"`,
`tabindex="0"` and its own key handling.

The `Loading` state replaces the visible label with the Loader, so the accessible name is held in
`aria-label` for the duration of the request; without it the control announces as an unnamed busy
element and a person who focuses it mid-request loses track of what they armed. The `Success` and
`Error` outcomes are published to a polite live region rather than being read off the colour change:
`Success` is terminal and stops offering the action, `Error` announces the failure and returns the
control to `Standard`, actionable again for another attempt.

The role is deliberately `button`, not `slider`. A `slider` implies `aria-valuenow` between a min and
a max that a person adjusts and leaves at a value; SlidingButton has no such value — the drag is a
means of committing, and the only outcomes are "armed" and "not armed". Reporting a running percentage
to assistive technology would describe the pointer animation, not the action.

---

## Focus

The root is the focusable element, and the only one — the Slider and the label take no focus of their
own, which keeps one stop per control.

| Requirement | Implementation |
|---|---|
| Reachable by Tab | `<button>` is focusable natively |
| Visible focus ring | `:focus-visible` — `s2` outline in `var(--text-and-icon-primary)`, radius `s20` to follow the track |
| Disabled | `aria-disabled` keeps it in the tab order; `disabled` removes it |
| Loading | Focus is retained; the control stays focusable while `aria-busy` is set |
| Skeleton | Not focusable — the placeholder is out of the tab order |

Focus is preserved across a `Loading` transition and across the resolution to `Success` or `Error`.
Moving focus away when the request starts would strand a keyboard or screen-reader user, since the
control they armed is the anchor for whatever the response reports.

---

## Keyboard

| Key | Behaviour |
|---|---|
| `Tab` / `Shift+Tab` | Moves focus to and from the control |
| `Enter` | Activates — arms and completes the action, the equivalent of a full drag |
| `Space` | Activates |

`Enter` and `Space` both activate, the platform behaviour a native `<button>` provides and a custom
element reproduces. There are no arrow-key semantics: the control is not a slider, so `Left` / `Right`
adjust nothing. In the `Disabled` and `Loading` states the activation keys have no effect, and
repeated presses while loading do not queue a second action.

---

## Contrast

Every pairing in § 6 of the spec resolves through semantic tokens, so Light and Dark are covered by
one name. The pairings worth verifying:

| Pairing | Note |
|---|---|
| `Background/InversePrimary` + `TextAndIcon/InversePrimary` | The resting label on the track; the pair flips together by theme, holding its contrast |
| `Background/Brand` + `TextAndIcon/AlwaysDark` | The Slider glyph, and the label while the Slider passes over it (the drag recolour) |
| `Accent/Red1` + `TextAndIcon/AlwaysLight` | The `Error` fill and its centred `close` glyph — the label is hidden in Error |
| `Surface/OnWhite` + `TextAndIcon/Disabled` | The lowest-contrast pairing by intent; stays above the 3:1 floor for a disabled control |

No state is signalled by colour alone: `Loading` sets `aria-busy` and swaps the label for the Loader,
`Success` and `Error` are announced through the live region, and `Disabled` is announced. The label
crossing from `InversePrimary` to `AlwaysDark` as the Slider covers it is a pointer-only transition —
verify both endpoints, since a low-vision pointer user sees the label on both surfaces.

---

## iOS · VoiceOver

`{action}` — the action alone (for example *Start ride*); "Slide" is omitted.

### Tap target · the control

| Attribute | Description |
|---|---|
| **Voice preview** | "{action}, Button." |
| **Label** | {action} |
| **Value** | n/a |
| **Traits** | Button |
| **Hint** | n/a |

**Notes:** "Slide" is omitted — with VoiceOver on a swipe navigates, so the word describes a gesture the user will not perform.

### Edge states

- **Loading:** the busy state is exposed (`aria-busy`); the Label is preserved and the Loader is not a separate element; the double-tap does nothing until it resolves.
- **Success:** posted as an announcement (`UIAccessibility.post(.announcement, …)`) — a product string such as *"Ride started."* The control is terminal.
- **Error:** posted as an announcement — e.g. *"Couldn't start the ride, try again."* The control returns to `Standard` and is activatable again.
- **Disabled (Dimmed):** `accessibilityTraits.notEnabled` — VoiceOver appends "Dimmed" and keeps it in the rotor so its state stays discoverable.
- **Skeleton:** `accessibilityElementsHidden` — the placeholder is not reachable.
- The 50% / 80% drag thresholds are not announced — VoiceOver activates the whole action on the double-tap.

---

## Android · TalkBack

### Tap target · the control

| Attribute | Description |
|---|---|
| **Voice preview** | "{action}, Button, Double-tap to activate." |
| **ContentDescription** | {action} |
| **Value / StateDescription** | n/a |
| **Role** | Button |
| **Action** | Double-tap to activate |

**Notes:** swipe performs a different interaction in TalkBack, so "Slide" is left out of the ContentDescription.

### Edge states

- **Loading:** StateDescription "Loading" (`aria-busy` → "Busy"), announced through a live region; the ContentDescription is preserved and the double-tap is ignored until it resolves.
- **Success:** announced once via `accessibilityLiveRegion` / `announceForAccessibility` — a product string such as *"Ride started."* The control is terminal.
- **Error:** announced the same way — e.g. *"Couldn't start the ride, try again."* The control returns to its `Standard` announcement and is actionable again.
- **Disabled:** *"{action}, Button, Disabled."* The Action is dropped; TalkBack skips it in swipe navigation and ignores the double-tap.
- **Skeleton:** not exposed (`importantForAccessibility = no`).
- The 50% / 80% drag thresholds are not announced — TalkBack commits the whole action on the double-tap.

---

## Target size

| Element | Height | Against WCAG 2.5.5 |
|---|---|---|
| Track (the whole control) | `s64` | Above the 48 the DS uses |

The control spans the width of its container, so the target is large in both axes; the SP scale modes
lift the height further (to 72 at 130%, 84 at 150%). The Slider is not a separate target — the whole
track is the one activation area for pointer and assistive technology alike.

---

## RTL

SlidingButton opts into RTL through its own `RTL` axis (`Off` by default), so the mirroring is part
of the component contract.

| Element | RTL=On |
|---|---|
| Slider position | Rests at the end (right) edge and travels right → left |
| Default icon | `forward-arrow-right` flips to point left — a directional glyph (see [`icon-container.md`](https://github.com/inDriver/oymyakon-ds/blob/main/specs/primitives/icon-container.md) § 3.5) |
| Label alignment | Centred — unchanged |
| Screen-reader announcement | Unchanged — the action name is semantic, not positional |

The result cycle is unaffected: the Success fill expands from the centre, and the Error return travels
back to whichever edge the Slider rests on.

---

## Testing checklist

- [ ] Tab reaches the control; the focus ring is visible against both Light and Dark backgrounds
- [ ] Both `Enter` and `Space` activate, and arrow keys do nothing
- [ ] Activation completes the action without requiring a pointer drag
- [ ] `Loading` sets `aria-busy` and preserves the accessible name via `aria-label`
- [ ] Focus is not moved away when the request starts or when it resolves
- [ ] Repeated activation while loading does not fire a second action
- [ ] `Success` and `Error` outcomes are spoken through the live region, not left to the fill colour
- [ ] After `Error` the control returns to `Standard` and is activatable again
- [ ] `Disabled` does not respond to `Enter` or `Space` and is announced, not shown by dimming alone
- [ ] `Skeleton` is absent from the accessibility tree
- [ ] The StartSlot arrow icon is `aria-hidden` and adds nothing to the announcement
- [ ] TalkBack announces label + "Button", with "Disabled" / "Busy" appended in those states
- [ ] VoiceOver announces label + Button trait, with "Dimmed" appended when disabled
- [ ] RTL=On rests the Slider at the end edge and flips the directional icon only
- [ ] `data-ds-state` matches the ARIA state after every transition

---

## References

**TalkBack (Android)**
- Turn on TalkBack — https://support.google.com/accessibility/android/answer/6007100
- Use TalkBack gestures — https://support.google.com/accessibility/android/answer/6151827

**VoiceOver (iOS)**
- Operate iPhone when VoiceOver is on — https://support.apple.com/en-kz/guide/iphone/iph3e2e2329/ios
- Use VoiceOver gestures on iPhone — https://support.apple.com/en-kz/guide/iphone/iph3e2e2281/26/ios/26

---

## Machine contract — `specs/components/sliding-button/capabilities.json`

Axes, allowed values, defaults, constraints and CSS/Figma bindings. Read this instead of guessing what the component can do.

```json
{
  "$schema": "../component-capabilities.schema.json",
  "id": "sliding-button",
  "name": "SlidingButton",
  "version": "3.0.8",
  "description": "A swipe-to-confirm action control: a track with a draggable Slider pulled edge to edge to commit, resolving into a Loading -> Success or Error result. One size; variability is the lifecycle state, RTL, an optional subtitle, and replaceable Title / Subtitle text styles.",
  "files": {
    "spec": "specs/components/sliding-button/sliding-button.md",
    "a11y": "specs/components/sliding-button/sliding-button-a11y.md",
    "preview": "src/sliding-button.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "Oymyakon (components)",
    "fileKey": "esor6S4RxgJnVh5hnzVFqD",
    "componentSets": {
      "[SlidingButton] 3.0": {
        "note": "Node 27954:1393. Published component-set key not captured — the Figma variable/key read required a live selection at authoring time. To be filled on the next Figma pass."
      }
    },
    "capturedAt": "2026-08-05"
  },
  "root": {
    "class": "sliding-button",
    "dataDsComponent": "sliding-button"
  },
  "anatomy": {
    "background": {
      "class": "sliding-button__background",
      "notes": "The track fill layer (z0), --background-inverse-primary at rest. During the drag it morphs toward --background-brand via a crossfade layer whose opacity is driven 0 -> 1 from 30% to 80% of the width (returns to 0 on release / reverse drag). Figma element 6."
    },
    "slider": {
      "class": "sliding-button__slider",
      "notes": "Carries the CustomSquircle base class .squircle alongside .sliding-button__slider in every state — the base carries the box model (radius --sp-s16, flex-centre, overflow, box-sizing); this modifier pins and sizes it and drives its behaviour. The draggable chip (z2), fill --background-brand, a fixed --sp-s56 x --sp-s56 at rest, pinned to the start edge, inset --sp-s4 from the track edges on all sides. Its width changes only on tap and on drag. Leading inner padding --sp-s16 at rest, --sp-s32 on tap — the padding step widens the Slider (to --sp-s72), so on tap the button grows, not only the icon. On drag the width follows the finger up to a maximum that keeps the Slider --sp-s4 clear of the right edge; the icon rides the leading edge, travelling in the drag direction. Fills the whole track (keeping the s4 inset), centred, in Loading/Success/Error. Figma element 1 (Slider) + element 3 (Slider Background)."
    },
    "startSlot": {
      "class": "sliding-button__start-slot",
      "notes": "The icon inside the Slider (default forward-arrow-right), --text-and-icon-always-dark on the brand fill. INSTANCE_SWAP. Figma element 2."
    },
    "text": {
      "class": "sliding-button__text",
      "notes": "StartText — the Title + Subtitle block (z3). Fills the track width: the Track (.sliding-button root) reserves --sp-s68 on each side, and StartText's own horizontal padding is --sp-s0. Vertical padding --sp-s4 keeps an s4 inset from the track top/bottom so the label never butts the track edges when it grows past one line. --sp-s2 gap between the two lines; each line is single-line, centred and truncated. Sits ABOVE the Slider so it stays visible during the drag; hidden in Loading/Success/Error."
    },
    "title": {
      "class": "sliding-button__title",
      "notes": "Centred label, Heading 4, --text-and-icon-inverse-primary, inside the StartText block. Rendered ABOVE the Slider (z3 > slider z2) so it stays visible as the Slider passes over it, crossing to --text-and-icon-always-dark once the Slider reaches 50% of the width. Hidden (opacity 0) in Loading, Success and Error — the result glyph takes the track. Figma element 4."
    },
    "subtitle": {
      "class": "sliding-button__subtitle",
      "notes": "Optional second line, Compact Body, inside the StartText block. Figma element 5."
    },
    "loader": {
      "class": "sliding-button__loader",
      "notes": "The Loading spinner (z4), Loader size L (--sp-s32 host), reproduced inline and rotating on --pattern-spin (like Button) until Loader ships its own spec."
    },
    "check": {
      "class": "sliding-button__check",
      "notes": "The Success glyph (z4), icons/outlined/actions/check.svg, --sp-s32, --text-and-icon-always-dark on the brand fill."
    },
    "close": {
      "class": "sliding-button__close",
      "notes": "The Error glyph (z4), icons/outlined/actions/close.svg, --sp-s32, --text-and-icon-always-light, centred on the Accent/Red1 fill. Shown in Error only; the label and start-slot icon are hidden, as in Success."
    },
    "skeleton": {
      "class": "sliding-button__skeleton",
      "notes": "A hosted Skeleton instance (carries the base .skeleton class + data-ds-component=skeleton, z5) mounted in the markup and shown only in the Skeleton state, filling the track shape (radius --sp-s20). The Slider, label, icon, result glyphs and track fill are removed behind it."
    }
  },
  "axes": {
    "rtl": {
      "title": "RTL",
      "type": "boolean",
      "default": false,
      "css": {
        "modifier": ".sliding-button--rtl"
      },
      "figma": {
        "kind": "variant-property",
        "property": "RTL",
        "values": {
          "false": "Off",
          "true": "On"
        }
      },
      "notes": "Rests the Slider at the end edge and travels the other way; the directional forward-arrow-right icon flips. Off by default."
    },
    "subtitle": {
      "title": "Subtitle",
      "type": "boolean",
      "default": true,
      "css": {
        "presence": ".sliding-button__subtitle"
      },
      "notes": "The optional second line under the Title. Present by default; a single-line Title is available with it absent."
    },
    "titleStyle": {
      "title": "Title style",
      "type": "enum",
      "default": "heading-4",
      "values": ["heading-4", "heading-3", "promo-body"],
      "css": {
        "mechanism": "heading-4 = bare .sliding-button (default, --text-heading-heading4-*); heading-3 = .sliding-button--title-heading-3 (--text-heading-heading3-*); promo-body = .sliding-button--title-promo-body (--text-body-promo-body-*)"
      },
      "figma": {
        "kind": "none",
        "notes": "A custom-base text-style override set in the Title's Auto Layout text properties, not a variant property."
      },
      "notes": "The Title text style — a replaceable default. Each option resets family + size + weight + line-height together per the typography rule."
    },
    "subtitleStyle": {
      "title": "Subtitle style",
      "type": "enum",
      "default": "compact-body",
      "values": ["compact-body", "main-body"],
      "css": {
        "mechanism": "compact-body = bare .sliding-button (default, --text-body-compact-body-*); main-body = .sliding-button--subtitle-main-body (--text-body-main-body-*)"
      },
      "figma": {
        "kind": "none",
        "notes": "A custom-base text-style override set in the Subtitle's Auto Layout text properties, not a variant property."
      },
      "notes": "The Subtitle text style — a replaceable default. Resets family + size + weight + line-height together."
    }
  },
  "states": {
    "default": [
      "standard",
      "on-tap",
      "on-drag",
      "loading",
      "success",
      "error",
      "disabled",
      "skeleton",
      "focus-visible"
    ]
  },
  "constraints": [
    "The role is button, not slider: the control commits one action and has no adjustable value to report. Assistive-technology and keyboard users never drag — Enter and Space activate, which arms and completes the action the same as a full drag; arrow keys do nothing.",
    "Three drag thresholds: from 30% of the track width the Background morphs from --background-inverse-primary toward --background-brand (complete at 80%); past 50% the Slider covers the label, which recolours to --text-and-icon-always-dark on --pattern-color; at 80% the gesture completes on release. Releasing at >=80% enters Loading; a release below 80% or a reverse drag returns the Background to --background-inverse-primary and springs the Slider back, returning the component to standard.",
    "Loading, Success and Error are a result cycle driven by the host resolving the armed action, not user-selectable variants. Loading follows a drag past 80% and resolves to exactly one of Success or Error. Success is terminal. Error shows a single close glyph (--sp-s32, --text-and-icon-always-light) centred on the Accent/Red1 fill — the label and start-slot icon hidden, as in Success — then returns to standard via a smooth retract: the close glyph fades out and the Slider animates its width back to rest on --component-fade-m-hide (script-driven, since CSS cannot transition to/from width:auto) rather than a hard state swap, actionable again.",
    "The slide action is emitted once, when the drag passes the 80% auto-completion threshold. The partial phases (on-tap, on-drag) are live states, not taps.",
    "The Loader inside Loading is Loader at size L (--sp-s32 host). Loader has no Markdown spec or shared.css build in this repo yet, so SlidingButton reproduces the arc geometry inline and consumes --pattern-spin until Loader ships.",
    "Disabled is a token swap, never an opacity multiplier (component-authoring rule): the track fill collapses to --surface-on-white, the Slider fill to --surface-on-grey, and the label and icon to --text-and-icon-disabled.",
    "No state is signalled by colour alone: Loading sets aria-busy, Success and Error are announced through a polite live region, Disabled is announced.",
    "aria-busy / aria-label and data-ds-state move together across the lifecycle, so the accessible value and the analytics value cannot drift apart. The accessible name is held in aria-label while the Loader replaces the label.",
    "The track hugs its container width — a swipe needs a travel distance, so unlike Button it does not hug its label. One size; track height --sp-s64, radius --sp-s20; the Track reserves --sp-s68 horizontal padding for the StartText label on each side, and StartText fills the remaining width (own padding --sp-s0), each line single-line and truncated. The Slider is a fixed --sp-s56 x --sp-s56 chip at rest, radius --sp-s16, inset --sp-s4 from every track edge, and never reaches the right edge (stays --sp-s4 clear). The result glyphs are --sp-s32 (check, close); the start-slot icon is --sp-s24.",
    "Motion follows the Figma Animation model, consuming DS tokens only — on tap --transforming-state-fast (Enter) / --pattern-scale-short-hide (Exit) on the leading padding; the drag on --curve-linear (direct manipulation, no timed token) with the label recolour + Background morph on --pattern-color; the on-drag return on --pattern-scale-medium-appear (Scale Medium); the result swaps and the Error tint / return on --component-fade-m-appear / --component-fade-m-hide (fadeM); the spinner on --pattern-spin. The track fill snaps (no transition) when entering Loading, so the s4 border shows no dark InversePrimary flash. All declared DS-wide in tokens/generated/motion.css and zeroed there under prefers-reduced-motion, so the component carries no override of its own.",
    "Haptic feedback (iOS): three transitions fire a haptic once at the moment the state is entered — On tap = Light Impact, Success = Success, Error = Error. Android plays no haptic.",
    "The reduced text-only / icon-only composition sketched in the Figma frame is not a contracted variant of 3.0.0 and is out of scope; if promoted it becomes a Type axis in a later version."
  ],
  "analytics": {
    "dataDsComponent": "sliding-button",
    "action": "slide",
    "valueAttr": "data-ds-state",
    "notes": "Coverage unit; the root is the only tap target and there are no internal targets. The slide action is emitted once at the 80% threshold. data-ds-state carries standard | on-tap | on-drag | loading | success | error | disabled | skeleton and is updated together with the matching ARIA attribute. Emits no user value."
  },
  "rtl": {
    "supported": true,
    "notes": "Opt-in through the RTL axis (Off by default). RTL=On rests the Slider at the end edge and flips the directional forward-arrow-right icon; the label stays centred and the accessible announcement is unchanged."
  }
}
```
