> ## 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/section-header.html
> Source: specs/components/section-header/section-header.md, specs/components/section-header/section-header-a11y.md, specs/components/section-header/capabilities.json

---

# SectionHeader · Oymyakon DS 3

> The title — and an optional subtitle — that opens a section of a screen: the screen itself, a
> bottom sheet, an island, or a group inside an island. Four size presets and two optional end
> slots.

**Version:** 3.0.0 · **Status:** Draft · **Figma:** `[SectionHeader] 3.0` (main components file
`7vdl5YkZFDWvh9QvSmydsH`, page `12903:482`; public master `12903:669`; size sets
`[XL-SectionHeader]` `13345:5943` · `[L-SectionHeader]` `13447:8146` · `[M-SectionHeader]`
`13309:8229` · `[S-SectionHeader]` `13345:6263`, each `State` (Enabled · Disabled · Skeleton) ×
`RTL`; the custom base `CustomSectionHeader` `13301:13710`; building blocks in the section
`12903:5060`; Specification frame `12906:4231`; the Update frame `27826:12644`)

---

## 1. Description

From the Specification frame: "A section header divides sections of a screen." It names what
follows — a screen, a bottom sheet, an island, a group of fields — with a title, and optionally a
subtitle under it.

It comes in four size presets that differ only in the title's text style — XL, L, M and S (§ 3) —
and a custom base whose every value can be re-pointed. Beside the text sit up to two **slots**
holding any DS component: an IconButton by default, a Button or a Squircle in the Figma examples.
Next to the title and the subtitle, small inline slots carry a 20 icon — a chevron that says the
text opens something, a hint, any icon.

### Platform implementations

| Platform | Entry point | Version | Notes |
|---|---|---|---|
| iOS | `DsSectionHeader(title:size:)` · `DsCustomSectionHeader(title:)` (DS3) | 3.0 | `size` = `.xl` / `.l` / `.m` / `.s`; subtitle, slots and layout as modifiers; Disabled = `.disabled(true)`, Skeleton = `.isLoading(true)` (iOS bridge notes) |
| Android | `DsSectionHeader` (`core.compose.ds.v3.section_header`) | 3.0 | `size = DsSectionHeader.Size.XL/L/M/S` (M default); `endInnerSlot` / `endOuterSlot` = the two slots; Disabled = `enabled = false`, Skeleton = `DsSkeletonState`; `onClick` makes the text a button (Android bridge notes) |
| Flutter | — | — | Not in a platform bridge; the Figma frame's badge reads "--" |
| Web | `src/shared/shared.css` (`.section-header`) | 3.0.0 | Reference implementation, this repo; preview page `src/section-header.njk` |

---

## 2. Anatomy

```
SectionHeader (.section-header)                       fill width · row · centred · gap s16
├── TextContainer (.section-header__text)             a heading · fill · column · gap s8
│   │   (Tap Area = Title+Icon: one button.section-header__tap inside it)
│   ├── TitleContainer (.section-header__line)        row
│   │   ├── Title (.section-header__title)            the size's text style
│   │   ├── TitleSlot#1 (.icon-container--20)         optional · IconContainer 20
│   │   └── TitleSlot#2 (.icon-container--20)         optional · IconContainer 20
│   └── SubtitleContainer (.section-header__line)     optional · row
│       ├── Subtitle (.section-header__subtitle)      Body Paragraph
│       ├── SubtitleSlot#1 (.icon-container--20)      optional · IconContainer 20
│       └── SubtitleSlot#2 (.icon-container--20)      optional · IconContainer 20
└── SlotsContainer (.section-header__slots)           optional · row · gap s4 · centred on the title line
    ├── Slot#1                                        hug · IconButton by default
    └── Slot#2                                        hug · IconButton by default
```

| Part | Required | Description |
|---|---|---|
| **Container** | Required | A row: TextContainer and SlotsContainer, `s16` apart. Padding `s20` top, `s12` bottom, `s20` on the sides (owner, 2026-11-12 — the masters draw `s24`, § 12). Background transparent — it sits on its host's surface |
| **Title** | Required (the custom base: on by default) | The size preset's text style (§ 7), `TextAndIcon/Primary`. Wraps to several lines when no TitleSlot is on; with a TitleSlot on, one line |
| **TitleSlot#1 / #2** | Optional, off by default | A 20 icon beside the title — `chevron-right`, `hint-outline`, any IconContainer. Any component may be placed (the frame) |
| **Subtitle** | Optional (the frame's custom base: off by default; the size presets stage it on) | Body Paragraph, `s8` under the title. Wraps when no SubtitleSlot is on; with one on, one line |
| **SubtitleSlot#1 / #2** | Optional, off by default | As the TitleSlots, beside the subtitle |
| **Slot#1 / Slot#2** | Optional, off by default | Hug width; centred on the title's first line (owner, 2026-11-12 — § 12). Default content: an IconButton (a 24 icon in an `s40` tap area, max 130 %). The frame's other preferred content: a [Button](https://super-dollop-pzmo65r.pages.github.io/button.md) S, Secondary style; a [Squircle](https://super-dollop-pzmo65r.pages.github.io/squircle.md), Surface style (its Standard). "You can place any components in slot" |

**Tap Area = Title+Icon** — optional, on by default when a title or subtitle slot holds the
chevron (the frame's "Tap Area" examples): the TextContainer becomes one button — the title, the
subtitle and their inline icons are its label. Off, the text is a heading only and each slot is
its own target.

### RTL layout

From the frame: in RTL the title and the subtitle align right; Slot#1 is placed second and Slot#2
first — the row mirrors as a whole. Directional inline icons (the chevron) mirror.

On the web: `dir="rtl"` on the root or an ancestor reverses the row; an inline icon that mirrors
is an [IconContainer](https://super-dollop-pzmo65r.pages.github.io/icon-container.md) with `.icon-container--rtl` (`scaleX(-1)` in RTL).

---

## 3. Variants and sizes

Figma axes: four size sets with `State` × `RTL`; the slot booleans (`SlotsContainer`, the inline
slots) on the instance.

| Axis | Values | Default | Web mechanism |
|---|---|---|---|
| **Size** | `xl` · `l` · `m` · `s` · `custom` | `m` (Android's default) | `.section-header--xl` / `--l` / (none = M) / `--s`; custom base: `.section-header--custom` (Heading 4 title) + `data-ds-preset="custom"` + the `--section-header-*` hooks |
| **Subtitle** | `on` · `off` | `on` in the size presets | The second `.section-header__line` present or absent |
| **Title / Subtitle slots** | 0 – 2 each | 0 | An IconContainer 20, Whole container (`.icon-container.icon-container--20.icon-container--whole`) inside the line; the line takes `--slotted` (one row) |
| **Slots** | 0 – 2 | 0 | `.section-header__slots` with one or two children |
| **Tap Area** | `on` · `off` | `off`; `on` when an inline slot holds the chevron (the frame) | `button.section-header__tap` inside the heading |
| **State** | `enabled` · `disabled` · `skeleton` | `enabled` | `.section-header--disabled` · `.section-header--skeleton` |
| **RTL** | `off` · `on` | `off` | `dir="rtl"` |

### Size presets — the title style only

Every preset shares the paddings and gaps (§ 8); only the title changes:

| Size | Title style | Use (frame, Usage context) |
|---|---|---|
| **XL** | Heading 1 — PP Agrandir Bold 32/36 | The main header of a whole screen |
| **L** | Heading 2 — PP Agrandir Bold 24/28 | A bottom sheet's title |
| **M** | Heading 3 — Suisse Intl Bold 20/28 | The main header of an island |
| **S** | Heading 4 — Suisse Intl Semibold 17/20 | A group's header inside an island |

### Custom base

`CustomSectionHeader` — the same structure with every value open ("All values can be
customized"); "any text styles can be applied". Its defaults: title Heading 4, subtitle Body
Paragraph, the same paddings and gaps as the presets.

| What | Web hook (unset = the preset's value) |
|---|---|
| Paddings | `--section-header-padding-top` (`var(--sp-100-s20)`) · `--section-header-padding-bottom` (`var(--sp-100-s12)`) · `--section-header-padding-inline` (`var(--sp-100-s20)`) |
| Gaps | `--section-header-gap` (`var(--sp-100-s16)`, text ↔ slots) · `--section-header-text-gap` (`var(--sp-100-s8)`) · `--section-header-slots-gap` (`var(--sp-100-s4)`) |
| Title style | `--section-header-title-family` / `-size` / `-weight` / `-line-height` (any DS text style's tokens) |
| Subtitle style | `--section-header-subtitle-family` / `-size` / `-weight` / `-line-height` |
| Colours | `--section-header-title-color` · `--section-header-subtitle-color` · `--section-header-background` (any semantic token) |

- An instance on the custom base carries `data-ds-preset="custom"` (§ 11).
- iOS: `DsCustomSectionHeader` with `titleFont` / `titleColor` / `subtitleFont` / `subtitleColor`
  and `layout`; Android: the same `DsSectionHeader` with `style = DsSectionHeader.Style(...)`.

---

## 4. States

| State | Figma | What changes |
|---|---|---|
| **Standard** | `State = Enabled` | Title and subtitle `TextAndIcon/Primary`; slots in their own Standard states |
| **Disabled** | `State = Disabled` | Title and subtitle `TextAndIcon/Disabled`; every nested component shows its own Disabled state — the IconContainers (`.icon-container--disabled`), a Button, a Squircle; nothing responds to taps |
| **Skeleton** | `State = Skeleton` | Two static bars — the title bar in the size's heading (L takes Heading 1's, as Figma draws it, § 8) up to 240 wide, the subtitle bar up to 200; no slots. "The skeleton is static and does not depend on the content. In Custom, you can define your own skeleton variant." |

"States such as Disabled, Skeleton the same for all presets" (the frame). No Pressed state is
staged: the tappable TextContainer and the IconButton show focus (§ 10), no press feedback.

---

## 5. Animation and behavior

Reference: [`motion-rules.md`](https://super-dollop-pzmo65r.pages.github.io/motion.md).

| Event | Pattern | Token | CSS |
|---|---|---|---|
| Tap — TextContainer (Tap Area on) | — | — | The host's navigation; the component emits the tap (§ 11). No press motion — Figma stages none |
| Tap — a slot | The slot component's own | Its spec | A Button presses with `pushButton`; the IconButton has no press motion staged |
| Standard ↔ Disabled | — | — | A swap of colours, no transition |
| Skeleton | `Patterns/Shimmer` | `var(--pattern-shimmer)` | The DS `.skeleton` bars' own sweep; zeroed under reduced motion at the token |
| Skeleton → Standard | — | — | A swap: the host replaces the bars with the text in one frame |

- Reduced motion: `motion.css` zeroes every `--pattern-*` token inside
  `prefers-reduced-motion: reduce`; the component carries no override.

---

## 6. Color tokens

| Element | Token | Notes |
|---|---|---|
| Background | — (transparent) | "transparent — by default"; custom base: `--section-header-background` |
| Title | `var(--text-and-icon-primary)` | |
| Subtitle | `var(--text-and-icon-primary)` | The component masters bind Primary; the frame's States list says `TextAndIcon/Secondary` — flagged (§ 12) |
| Inline slot icons | `var(--text-and-icon-primary)` | The IconContainer's own colour; Disabled: `.icon-container--disabled` |
| IconButton icon | `var(--text-and-icon-primary)` | The IconContainer's own colour; Disabled: `.icon-container--disabled` |
| Button in a slot | The Button's Secondary style | `var(--surface-on-white)` · `var(--text-and-icon-primary)` |
| Squircle in a slot | The Squircle's Standard ("Surface") | `var(--surface-on-white)` · `var(--text-and-icon-primary)` |
| Disabled — title, subtitle, IconButton | `var(--text-and-icon-disabled)` | |
| Skeleton bars | `var(--skeleton-on-white)` · shimmer `var(--skeleton-wave)` | DS `.skeleton` |

- Contrast: `TextAndIcon/Primary` on `Background/Primary` ≈ 15.7:1 Day. Disabled
  `TextAndIcon/Disabled` sits under 4.5:1 by design — the state is also announced (§ 10).

---

## 7. Typography

| Element | Style | Tokens | Scaling |
|---|---|---|---|
| Title — XL | Heading 1 — PP Agrandir Bold 32/36 | `var(--text-heading-heading1-*)` | `fixed` (Figma binds `SP/100% ratio`) |
| Title — L | Heading 2 — PP Agrandir Bold 24/28 | `var(--text-heading-heading2-*)` | `cap130` (`SP/130% ratio`) |
| Title — M | Heading 3 — Suisse Intl Bold 20/28 | `var(--text-heading-heading3-*)` | `cap130` (`SP/130% ratio`) |
| Title — S, custom base | Heading 4 — Suisse Intl Semibold 17/20 | `var(--text-heading-heading4-*)` | `full` (`SP/150% ratio`) |
| Subtitle | Body Paragraph — Suisse Intl Book 16/24 | `var(--text-body-body-paragraph-*)` | `full` (`SP/150% ratio`) |

- PP Agrandir has no Cyrillic — XL and L titles fall back to Suisse Intl Bold through the font
  stack (`typography` rule).
- The title wraps when no TitleSlot is on and keeps to one line with a TitleSlot on; the same for
  the subtitle and its SubtitleSlots. "Additionally, when TitleSlot is disabled, the text can be
  limited to one line" — the host's choice.

---

## 8. Spacing

Figma binds every padding and gap to `SP/100% ratio` (fixed) — the header keeps its geometry at
130 / 150 % while the text grows.

| Property | Token | @100% | Figma variable | Notes |
|---|---|---|---|---|
| Width | — | fills the host | — | 360 in the Figma sets; Flexible (iOS) / FillMax (Android) |
| Padding — top | `var(--sp-100-s20)` | 20 | `SP/100% ratio/s20` | `--section-header-padding-top` |
| Padding — bottom | `var(--sp-100-s12)` | 12 | `SP/100% ratio/s12` | `--section-header-padding-bottom` |
| Padding — start / end | `var(--sp-100-s20)` | 20 | — (the masters bind `SP/100% ratio/s24`) | `--section-header-padding-inline`. Owner, 2026-11-12: `s20`, as the OrderDetails Block draws it |
| TextContainer ↔ SlotsContainer | `var(--sp-100-s16)` | 16 | `SP/100% ratio/s16` | `--section-header-gap` |
| Title ↔ subtitle | `var(--sp-100-s8)` | 8 | `SP/100% ratio/s8` | `--section-header-text-gap`; the Disabled variants draw `s4` — flagged (§ 12) |
| Slot#1 ↔ Slot#2 | `var(--sp-100-s4)` | 4 | `SP/100% ratio/s4` | `--section-header-slots-gap` |
| SlotsContainer height | the title's line box | 28 (M) | — | `--section-header-title-size` × `--section-header-title-line-height`: the slots centre on it; an `s40` slot overflows it evenly (6 above and below on M, 10 on S — inside the `s20` top padding), so the slots add no height |
| Text ↔ inline slot | — | 0 | `SP/100% ratio/s0` | |
| Inline slot icon | `var(--sp-s20)` | 20 | — | An IconContainer 20, Whole container — scales with the text (the frame's 150 % example) |
| IconButton tap area | `var(--sp-130-s40)` | 40 | — | "tap area s40 (max 130%)" |
| IconButton icon | `var(--sp-s24)` | 24 | — | An IconContainer 24 (its own size, full) |
| Skeleton — title bar | XL and L `.skeleton-text--heading1` · M `--heading3` · S `--heading4` | — | — | Max width 240 (`--section-header-skeleton-title-max`, off the SP grid). L takes the Heading 1 bar as Figma draws it (`13447:8159`) |
| Skeleton — subtitle bar | `.skeleton-text--main-body` | 16 + `s2` / `s2` | — | Max width 200 (`--section-header-skeleton-subtitle-max`) |

---

## 9. Usage context

### When to use SectionHeader

From the frame's Usage context:

- **XL** — the main header for the entire screen ("Freight" over the service picker).
- **L** — a bottom sheet's title ("Raise your fare to get drivers' attention").
- **M** — the main header of an island ("Payment", "Ride details").
- **S** — the header of a group inside an island ("Pickup time").

### When not to use

- As the screen's navigation bar — that is the NavBar.
- For a row the user acts on — use a [Cell](https://super-dollop-pzmo65r.pages.github.io/cell.md); a SectionHeader names a section, its
  tap opens that section's details at most.
- Two SectionHeaders of the same size back to back with nothing between them.

### Related components

- [Button](https://super-dollop-pzmo65r.pages.github.io/button.md) — S Secondary in a slot.
- [Squircle](https://super-dollop-pzmo65r.pages.github.io/squircle.md) — Standard in a slot.
- [IconContainer](https://super-dollop-pzmo65r.pages.github.io/icon-container.md) — the inline slot icons.
- [Skeleton](https://super-dollop-pzmo65r.pages.github.io/skeleton.md) — the loading bars.
- [OrderDetails](https://github.com/inDriver/oymyakon-ds/blob/main/specs/blocks/order-details/spec.md) — a Block that opens with an M SectionHeader.

---

## 10. Accessibility

Full spec: [`section-header-a11y.md`](https://super-dollop-pzmo65r.pages.github.io/section-header.md). From the frame (TalkBack and
VoiceOver):

- **Tap Area on** — the TextContainer is one element: label "[Title], [Subtitle]", role Button and
  heading (Android `isHeading = true`; iOS traits `.header`, `.button`). Disabled: "…, disabled" /
  "…, dimmed".
- **Tap Area off** — the text is one heading element, "[Title], [Subtitle], Heading"; each slot is
  its own Button with its own label (an IconButton's alt text, a Button's label).
- Inline slot icons are decorative — part of the text element, not announced.
- Focus order: the TextContainer, then Slot#1, then Slot#2 (reading order — mirrored in RTL).
- Resizing: supported — staged at 100 % and 150 %.

---

## 11. Analytics and coverage contract

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

| Field | Value |
|---|---|
| `data-ds-component` | `section-header` |
| Coverage unit | Yes |
| Tap target model | Multiple targets — the TextContainer (Tap Area on) and each IconButton slot; a Button / Squircle in a slot is its own component with its own contract |
| Actions | `tap` on the TextContainer (`data-ds-target="text"`); `tap` on an IconButton (`data-ds-target="slot-1"` / `"slot-2"`) |
| Emits a value | No |
| `data-ds-preset` | `xl` · `l` · `m` · `s` · `custom` |
| `data-ds-state` | `standard` · `disabled` · `skeleton` |

Required prototype markup:

```html
<div class="section-header section-header--l" data-ds-component="section-header"
     data-ds-component-id="sheet-title" data-ds-preset="l" data-ds-state="standard">
  <h2 class="section-header__text">
    <button type="button" class="section-header__tap" data-ds-action="tap" data-ds-target="text">
      <span class="section-header__line section-header__line--slotted">
        <span class="section-header__title">Raise your fare</span>
        <span class="icon-container icon-container--20 icon-container--whole icon-container--rtl" aria-hidden="true"><!-- chevron-right --></span>
      </span>
      <span class="section-header__line"><span class="section-header__subtitle">Drivers see higher fares first</span></span>
    </button>
  </h2>
  <div class="section-header__slots">
    <button type="button" class="section-header__icon-button" aria-label="Info"
            data-ds-action="tap" data-ds-target="slot-1"><span class="icon-container"><!-- hint-outline --></span></button>
  </div>
</div>
```

Disabled — every button keeps `aria-disabled="true"` (still focusable, as on the platforms):

```html
<div class="section-header section-header--l section-header--disabled" data-ds-component="section-header"
     data-ds-component-id="sheet-title" data-ds-preset="l" data-ds-state="disabled">
  <h2 class="section-header__text">
    <button type="button" class="section-header__tap" aria-disabled="true" data-ds-action="tap" data-ds-target="text">…</button>
  </h2>
  <div class="section-header__slots">
    <button type="button" class="section-header__icon-button" aria-label="Info" aria-disabled="true"
            data-ds-action="tap" data-ds-target="slot-1"><span class="icon-container icon-container--disabled">…</span></button>
  </div>
</div>
```

- **Implementation trap.** `aria-disabled` does not stop a native `<button>`: it still fires a click on a
  tap, on Enter and on Space. The host refuses activation of an `aria-disabled="true"` button in the
  capture phase, before any analytics listener counts it as a `tap` (the preview page does this in its
  own script); `shared.css` carries the styling only.

---

## 12. Open in Figma

- **Subtitle colour** — the masters bind `TextAndIcon/Primary`; the frame's States list says
  `TextAndIcon/Secondary`. The web follows the masters.
- **Disabled title ↔ subtitle gap** — `s4` in every Disabled variant, `s8` in Enabled, so a
  Disabled header is 4 shorter. The web keeps `s8` (a state does not move the layout).
- **L Skeleton** — the title bar is the Heading 1 bar (30 high, as on XL), not Heading 2's (24), so the L
  Skeleton is 98 high, 6 taller than the L header. The web follows Figma.
- **Disabled with Tap Area off** (owner's call) — the frame dims the title and the subtitle
  (`TextAndIcon/Disabled`, below 4.5:1 by design) while TalkBack and VoiceOver read the heading as in
  Standard: a sighted low-vision user sees faded text that nothing announces as disabled. Keep as the
  frame stages it, or keep Primary text when the heading is not a control?
- **Side padding** — the masters bind `s24`; the owner set `s20` (2026-11-12), the value the OrderDetails Block
  already uses. The web follows the ruling; the masters need the update.
- **Slot alignment** — the masters top-align the slots (SlotsContainer `items-start`), so an `s40` slot sits
  6 below the title's centre on M and 10 on S, and a title-only header with a slot grows to the slot's height.
  The owner ruled the slots centred on the title's first line (2026-11-12) — the web follows the ruling; the
  masters need the update.
- **The frame's custom-base text** lists "Heading 4 — title style" and "Body Paragraph — subtitle
  style" as defaults; the size presets are the reference for XL – S.

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.0.0 | 2026-11-12 | Initial spec and web build, from the Figma `[SectionHeader] 3.0` page (`12903:482`): the four size sets (XL Heading 1 · L Heading 2 · M Heading 3 · S Heading 4, one shared geometry on `SP/100% ratio`), the custom base, the Specification frame `12906:4231` (description, slots, states, layout, RTL, resizing, TalkBack and VoiceOver, usage context). Built for the OrderDetails Block (`specs/blocks/order-details/`) |

---

# SectionHeader — Accessibility

> Companion to [`section-header.md`](https://super-dollop-pzmo65r.pages.github.io/section-header.md) § 10. Restates the Figma Specification
> frame's "Accessibility. Android (TalkBack)" and "Accessibility. iOS (VoiceOver)" blocks
> (`12906:5028`, `13447:20672`) and its resizing block (`12906:5013`).

## Two configurations

The frame stages two configurations, each Standard and Disabled:

| # | Configuration | Tap Area | Elements in focus order |
|---|---|---|---|
| 1 | Title + Subtitle + TitleSlot (a chevron) | on | One — the TextContainer |
| 2 | Title + Subtitle + Slot#1 + Slot#2 | off | Three — the TextContainer, Slot#1, Slot#2 |

- The TextContainer is always a **heading**. With Tap Area on it is also a **button**.
- The title and the subtitle are read as one element — "[Title], [Subtitle]" — never as two stops.
- Inline slot icons (TitleSlot, SubtitleSlot) are decorative: part of the text element, not
  announced, not focusable.
- Every graphic in a slot is labelled by its alt text (an IconButton) or its own label (a Button).

## Focus order

TextContainer → Slot#1 → Slot#2 — reading order. In RTL the row mirrors, and so does the order a
sighted user follows; assistive technology keeps the logical order (text first).

---

## Android · TalkBack

Trait = the Android role (Button, Heading, `isHeading`); Hint = the frame's voiced preview.

| Element | Label (ContentDescription) | Value | Trait | Hint |
|---|---|---|---|---|
| TextContainer, Tap Area on | [Title], [Subtitle] | — | Button, `isHeading = true` | "Fast courier delivery, Send anything safely across the city, Button, Double-tap to activate" |
| TextContainer, Tap Area off | [Title], [Subtitle] | — | Heading | "Complete weekly tasks, Earn 150 coins — 3 days left, Heading" |
| Slot — IconButton | [Alt-text] | — | Button | "Info, Button, Double-tap to activate" |
| Slot — Button | [Label] | — | Button | "120, Button, Double-tap to activate" |

### Edge states

- **Disabled** — every element stays in the focus order and ends with "disabled": "Fast courier
  delivery, Send anything safely across the city, Button, disabled"; "Info, Button, disabled";
  "120, Button, disabled". A heading with Tap Area off reads as in Standard.
- **Skeleton** — the bars are hidden from TalkBack; the host announces the loading.

---

## iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| TextContainer, Tap Area on | [Title], [Subtitle] | — | `.header`, `.button` | "Fast courier delivery, Send anything safely across the city, Button" |
| TextContainer, Tap Area off | [Title], [Subtitle] | — | `.header` | "Complete weekly tasks, Earn 150 coins — 3 days left, Heading" |
| Slot — IconButton | [Alt-text] | — | `.button` | "Info, Button" |
| Slot — Button | [Label] | — | `.button` | "120, Button" |

### Edge states

- **Dimmed** — "…, dimmed, Button": "Fast courier delivery, Send anything safely across the city,
  dimmed, Button"; "Info, dimmed, Button"; "120, dimmed, Button". A heading with Tap Area off reads
  as in Standard.
- **Skeleton** — the bars are hidden from VoiceOver; the host announces the loading.

---

## Resizing

"Resizing is supported in the components" — staged at 100 % and 150 %. The text grows by its
style's mode (§ 7 of the main spec: XL fixed, L / M cap130, S and the subtitle full); the paddings
and gaps stay fixed; the IconButton's tap area stops at 130 %. A title with a TitleSlot keeps to one
line and truncates; without one it wraps.

- **Truncation at 150 %** — a slotted line keeps to one row and truncates at any size, as the frame
  stages it (accepted: the Figma contract). The accessible name stays complete.
- **Touch targets** — the IconButton's `s40` tap area is accepted DS-wide (owner, 2026-11-12:
  S-size targets at 40, as Button S — `typography-rules` § 12). The Tap Area is the title + icon
  area (the frame), as tall as the text; a hit area beyond the text is not specified.

---

## Web preview (reference)

- The TextContainer is a heading element; the host sets its level (`h1` for an XL screen title,
  `h2` / `h3` below it). Tap Area on puts one `<button>` inside the heading, so the heading and the
  button are one stop with the label "[Title] [Subtitle]".
- Inline icons carry `aria-hidden="true"`.
- An IconButton is a `<button>` with `aria-label` (the alt text). Disabled: `aria-disabled="true"`
  (it stays focusable, as on the platforms) and its activation is refused.
- **Trap:** `aria-disabled` does not stop a native `<button>` — it still fires a click on a tap, on
  Enter and on Space. The host refuses the activation in the capture phase, before analytics counts a
  `tap` (the preview page does it in its own script; `shared.css` styles only).
- Skeleton: the bars carry `aria-hidden="true"`.
- Focus: a `var(--sp-s2)` `TextAndIcon/Primary` outline on the button in focus.

---

## Machine contract — `specs/components/section-header/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": "section-header",
  "name": "SectionHeader",
  "version": "3.0.0",
  "description": "The title — and an optional subtitle — that opens a section of a screen: XL the screen, L a bottom sheet, M an island, S a group inside an island. One geometry for every size (paddings and gaps SP/100%); the size changes the title style only. Up to two end slots (IconButton by default) centred on the title's first line, and two inline 20 slots beside the title and the subtitle. The text is a heading; with Tap Area on it is also one button.",
  "files": {
    "spec": "specs/components/section-header/section-header.md",
    "a11y": "specs/components/section-header/section-header-a11y.md",
    "preview": "src/section-header.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "🕹️ Oymyakon (components) — main file",
    "fileKey": "7vdl5YkZFDWvh9QvSmydsH",
    "componentSets": {
      "[XL-SectionHeader]": { "key": "ca2d08c072693279d6f48da220704a7d79da0896", "note": "Set 13345:5943 — State × RTL." },
      "[L-SectionHeader]": { "key": "12694a92c3af7e8e64c3ccb6ead12f99eca981c5", "note": "Set 13447:8146 — State × RTL." },
      "[M-SectionHeader]": { "key": "ad29ee2c6de55cc62d2bb7a911134fc1acc0c931", "note": "Set 13309:8229 — State × RTL. The default size." },
      "[S-SectionHeader]": { "key": "82a5fc0694e442abddc62047af3d38251ebfdb2a", "note": "Set 13345:6263 — State × RTL." },
      "CustomSectionHeader": { "key": "6a34e4ae288789f3f9d51dc4746683a848988492", "note": "The custom base 13301:13710; public master [SectionHeader] 3.0 12903:669, page 12903:482, Specification frame 12906:4231." }
    },
    "capturedAt": "2026-11-12"
  },
  "root": {
    "class": "section-header",
    "dataDsComponent": "section-header"
  },
  "anatomy": {
    "textContainer": {
      "class": "section-header__text",
      "notes": "A heading element (the host sets the level: h1 for an XL screen title, h2 / h3 below); fills the row; title over subtitle, s8 apart."
    },
    "tapArea": {
      "class": "section-header__tap",
      "optional": true,
      "notes": "Tap Area = Title+Icon: one <button> inside the heading — the title, the subtitle and their inline icons are its label."
    },
    "line": {
      "class": "section-header__line",
      "notes": "A text line with its inline slots; .section-header__line--slotted keeps it to one row with an ellipsis."
    },
    "title": {
      "class": "section-header__title",
      "notes": "The size's Heading style through --section-header-title-*; wraps unless a TitleSlot is on."
    },
    "subtitle": {
      "class": "section-header__subtitle",
      "optional": true,
      "notes": "Body Paragraph; wraps unless a SubtitleSlot is on."
    },
    "slots": {
      "class": "section-header__slots",
      "optional": true,
      "notes": "Slot#1 then Slot#2, s4 apart, centred on the title's first line (owner, 2026-11-12); adds no height to the header."
    },
    "iconButton": {
      "class": "section-header__icon-button",
      "optional": true,
      "notes": "The slots' default content — an IconContainer 24 in an s40 tap area (max 130 %), aria-label = the alt text."
    }
  },
  "axes": {
    "size": {
      "title": "Size",
      "type": "enum",
      "values": ["xl", "l", "m", "s", "custom"],
      "default": "m",
      "css": {
        "mechanism": "M is the bare .section-header; .section-header--xl / --l / --s set the --section-header-title-* properties to Heading 1 / 2 / 4; .section-header--custom = the custom base (Heading 4 by default) with data-ds-preset=\"custom\" and the instance's own --section-header-* values"
      },
      "figma": { "kind": "component-set", "notes": "One set per size: [XL-SectionHeader] · [L-SectionHeader] · [M-SectionHeader] · [S-SectionHeader]; custom = CustomSectionHeader." },
      "constraints": [
        "XL — the main header of a whole screen, one per screen; L — a bottom sheet's title; M — an island's header; S — a group's header inside an island.",
        "XL and L titles are PP Agrandir (no Cyrillic — Suisse Intl Bold through the font stack)."
      ]
    },
    "subtitle": {
      "title": "Subtitle",
      "type": "boolean",
      "default": true,
      "css": { "mechanism": "presence of the second .section-header__line with .section-header__subtitle" },
      "figma": { "kind": "none", "notes": "The size presets stage it on; off by default on the custom base." }
    },
    "titleSlots": {
      "title": "Title slots",
      "type": "number",
      "min": 0,
      "max": 2,
      "step": 1,
      "default": 0,
      "css": { "mechanism": "IconContainer 20, Whole container (.icon-container.icon-container--20.icon-container--whole) after .section-header__title; the line takes .section-header__line--slotted" },
      "figma": { "kind": "none", "notes": "TitleSlot#1 / #2 booleans in the TitleContainer; chevron-right, hint-outline or any icon." },
      "constraints": ["Decorative (aria-hidden) — part of the text element.", "A chevron takes .icon-container--rtl and mirrors."]
    },
    "subtitleSlots": {
      "title": "Subtitle slots",
      "type": "number",
      "min": 0,
      "max": 2,
      "step": 1,
      "default": 0,
      "css": { "mechanism": "IconContainer 20, Whole container after .section-header__subtitle; the line takes .section-header__line--slotted" },
      "figma": { "kind": "none", "notes": "SubtitleSlot#1 / #2 booleans in the SubtitleContainer." }
    },
    "slots": {
      "title": "End slots",
      "type": "number",
      "min": 0,
      "max": 2,
      "step": 1,
      "default": 0,
      "css": { "mechanism": "children of .section-header__slots — .section-header__icon-button (default), a Button S Secondary, a Squircle S inside the icon-button wrapper, or any DS component" },
      "figma": { "kind": "none", "notes": "SlotsContainer boolean; Slot#1 / Slot#2 instance swaps." },
      "constraints": ["Each slot is its own target with its own label.", "Centred on the title's first line; the Figma masters top-align them — flagged in the spec."]
    },
    "tapArea": {
      "title": "Tap Area",
      "type": "boolean",
      "default": false,
      "css": { "mechanism": "button.section-header__tap inside the heading" },
      "figma": { "kind": "none", "notes": "Tap Area = Title+Icon (optional; on by default when an inline slot holds the chevron)." },
      "constraints": ["Off by default; on when an inline slot holds the chevron (the frame) — the text then opens the section's details.", "aria-disabled does not stop a native <button>: the host refuses activation of a disabled tap target in the capture phase, before analytics."]
    },
    "titleColor": {
      "title": "Title colour",
      "type": "token",
      "tokens": ["--text-and-icon-primary"],
      "customizable": "custom base only — any TextAndIcon/* token, 4.5:1 or more on the host's surface",
      "default": "--text-and-icon-primary",
      "css": { "customProperty": "--section-header-title-color" },
      "figma": { "kind": "none" }
    },
    "subtitleColor": {
      "title": "Subtitle colour",
      "type": "token",
      "tokens": ["--text-and-icon-primary"],
      "customizable": "custom base only — any TextAndIcon/* token, 4.5:1 or more on the host's surface",
      "default": "--text-and-icon-primary",
      "css": { "customProperty": "--section-header-subtitle-color" },
      "figma": { "kind": "none", "notes": "The masters bind TextAndIcon/Primary; the frame's list says Secondary — flagged." }
    },
    "paddingInline": {
      "title": "Side padding",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-100-s20",
      "customizable": "custom base only — any SP token",
      "css": { "customProperty": "--section-header-padding-inline" },
      "figma": { "kind": "layout", "notes": "The masters bind SP/100% ratio/s24; owner set s20 (2026-11-12)." }
    },
    "rtl": {
      "title": "RTL",
      "type": "boolean",
      "default": false,
      "css": { "mechanism": "dir=\"rtl\" on the root or an ancestor — the row mirrors (Slot#2 first), the text aligns right" },
      "figma": { "kind": "variant-property", "property": "RTL" }
    }
  },
  "states": {
    "default": ["standard", "disabled", "skeleton"]
  },
  "analytics": {
    "dataDsComponent": "section-header",
    "actions": ["tap"],
    "targets": ["text", "slot-1", "slot-2"],
    "notes": "Coverage unit. data-ds-preset = xl · l · m · s · custom; data-ds-state = standard · disabled · skeleton. A Button or Squircle in a slot is its own component with its own marks."
  },
  "rtl": {
    "supported": true,
    "notes": "The row mirrors as a whole; a chevron in an inline slot mirrors (.icon-container--rtl)."
  },
  "constraints": [
    "One geometry for every size: padding s20 top, s12 bottom, s20 start / end (all SP/100%), s16 text to slots, s8 title to subtitle, s4 slot to slot.",
    "Three states: Standard, Disabled (text TextAndIcon/Disabled, every nested component in its own Disabled), Skeleton (two static bars, no slots; L draws the Heading 1 bar, as Figma does).",
    "No motion of its own — states swap in one frame; a Button in a slot keeps its pushButton.",
    "The TextContainer is always a heading; the title and the subtitle are read as one stop, never two.",
    "Not the screen's navigation bar (NavBar); not a row the user acts on (Cell); not two headers of one size back to back."
  ]
}
```
