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

---

# PasswordField · Oymyakon DS 3

> The password field: the TextField anatomy with locked password content — a masked input and
> the show-password toggle fixed in the end slot.

**Version:** 3.1.1 · **Status:** Ready · **Figma:** `[PasswordField] 3.1` (master `13282:23022`, set `8216:7548`, Specification frame `13282:23071`)

---

## 1. Description

PasswordField is the Password Input of the text-field family — an assembly on the
[TextField](https://super-dollop-pzmo65r.pages.github.io/text-field.md) anatomy whose parts are pre-assigned: the
[BaseTextLine](https://super-dollop-pzmo65r.pages.github.io/base-text-line.md) core carries the entered password **masked
with bullets (`•`) by default**, the end slot permanently hosts the **Show Password Button**
(the `eye-visibility-on` ⇄ `eye-visibility-off` icons), and the Helper line sits below. The
label defaults to "Password". The Figma Specification states it plainly: *"This is a separate
component and is not part of a standard TextField."*

The end slot is **not configurable** — both platforms hardwire the toggle there (the iOS
manifest note says "do not pass one"). Slot#1 is the only open slot: *"Only icons from the DS
can be placed. By default, it should be disabled."*

The reveal is a **persistent toggle**: pressing the eye shows the entered text and the icon
flips to `eye-visibility-off`; pressing again re-masks. There is no press-and-hold reveal, no
auto re-mask timer, and no re-mask on focus loss — on either platform or the web.

### Platform implementations

| Platform | Entry point | Version | Notes |
|---|---|---|---|
| iOS | `Modules/DS3/DS3/Sources/TextField/DsSecureTextField.swift` | 3.1 (Figma line) | `DsSecureTextField` — composes `DsPortedTextField` directly with its own chrome (not `DsTextField`); masking is the native `isSecureTextEntry`; `initialPasswordHidden` seeds the state |
| Android | `core/compose/…/ds/v3/textfield/password/DsPasswordTextField.kt` | 3.1 (`@DsComponentInfo`) | `DsPasswordTextField` — a wrapper over `DsCustomTextField` 3.2; masking is a display-only `OutputTransformation` (`•` × length) over a raw `TextFieldState` |
| Flutter | — | — | Not shipped (the Figma cover badge says `--`) |
| Web | `src/shared/shared.css` (`.text-field` + `.password-field`) | 3.1.0 | The TextField block plus `type="password"` masking and the toggle; preview page `src/password-field.njk` |

---

## 2. Anatomy

The TextField anatomy with locked content — differences only:

```
[PasswordField] (public master)             outer margins s4 vert / s16 horiz
└── PasswordField (.text-field.password-field)   the TextField root, password content locked
    ├── box (.text-field__box)
    │   ├── Slot#1 (optional, off by default)     a DS icon, s40 container / s24 icon
    │   ├── StartText
    │   │     ├── label — "Password" by default
    │   │     └── BaseTextLine: the masked value (••••) · the caret
    │   └── end slot → Show Password Button (.password-field__toggle)   s48 tap, s24 eye icon
    └── Helper (.text-field__description)
```

| Part | Required | Description |
|---|---|---|
| **Slot#1** | Off (**default**) · on | "Only icons from the DS can be placed. By default, it should be disabled" — `s40 × s40` container, `s24` icon, `TextAndIcon/Primary` |
| **label** | Required | Default text "Password"; compact mode places it above the text, non-compact makes it the placeholder (the TextField float contract) |
| **Input text** | — | The entered password. "By default, it is hidden" — masked with `•` bullets, Main Body |
| **Show Password Button** | Required, fixed | "Button to display the entered text." Tap `s48 × s48`, icon `s24 × s24`, `TextAndIcon/Primary`; `eye-visibility-on` while hidden, `eye-visibility-off` while visible. Present in every state, including empty and disabled |
| **Caret** | — | "The caret adopts the Input Text color style" |
| **Active Indicator** | — | The `s2` inside border on active (`Border/Active`) and error (`Border/Error`) |
| **Helper** | Off · on by default | The Description contract from TextField — hint text, error text with the 16 error icon |

### RTL layout

The chrome mirrors as in TextField. The masked value is symmetric bullets, so no forced-LTR
line is staged for the value; the toggle moves to the start edge in RTL.

---

## 3. Variants and sizes

| Axis | Values | Default | Web mechanism |
|---|---|---|---|
| **State** | `standard empty` · `active empty` · `active filled` · `standard filled` · `skeleton` (Figma: `rest …`) | `standard empty` | As TextField. **No `data loading` State** — unlike PhoneField, the set does not stage one |
| **Visibility** | `hidden` · `visible` | `hidden` | `type="password"` ⇄ `type="text"` + the icon swap — a runtime toggle, staged in Figma via the `password-button-show`/`-hide` swap |
| **Disabled** | off · on | off | `.text-field--disabled` |
| **Validation** | `none` · `error` | `none` | `.text-field--error` |
| **RTL** | off · on | off | `dir="rtl"` |
| **Slot#1** | off · on | **off** in usage (the Figma boolean stages true) | Presence of the start-slot icon |
| **Helper** | off · on | **on** | Presence of `.text-field__description` |
| **Label mode** | `compact` · `placeholder` | `compact` | The TextField contract; default text "Password" |

The colour, border, skeleton, error and label-float contracts are TextField's — nothing
re-declared.

---

## 4. States

As TextField, with the password content:

| State | What differs from TextField |
|---|---|
| **Standard empty** (Figma: rest empty) | The label centred; the eye is **already there** (the toggle never hides) |
| **Active empty** | The label floats, the caret appears |
| **Active filled / Standard filled** | The masked bullets (`••••••••`); revealing shows the plain text (the staging shows `12345678`) |
| **Password visible** | The value in plain text, the icon flips to `eye-visibility-off`; persists until toggled back — no auto re-mask |
| **Error** | Red border + the Helper turns `TextAndIcon/Error` with the error icon; "The red border does not disappear until the next validation" |
| **Disabled** | Everything tints `TextAndIcon/Disabled`, the toggle stays visible but non-interactive |
| **Skeleton** | The whole box, as TextField |

- **No loading state.** The Figma set has no `data loading` State and no loader staging. Android
  nevertheless inherits an `isLoading` knob from its base that swaps the toggle for a loader —
  a shipped extra outside the Figma contract, recorded below.

---

## 5. Animation and behavior

Everything is the TextField motion contract (Scale Medium label float and border fade, no press
animation on the field, shimmer) — see [`text-field.md`](https://super-dollop-pzmo65r.pages.github.io/text-field.md) § 5 and
[`motion-rules.md`](https://super-dollop-pzmo65r.pages.github.io/motion.md).

- **The mask/unmask swap is instant** on both platforms (an icon and render swap, no
  transition) — the web follows: no animation on the toggle.
- iOS animates its compact-placeholder float with `standardEaseInOut` at **250 ms** (`medium1`)
  — a departure from the family's 400 ms Scale Medium that both TextField builds use; recorded.

### Recorded platform divergences

- **Masking mechanism**: iOS uses the OS secure mode (`isSecureTextEntry` — the system also
  suppresses copy/cut while masked); Android masks display-only via `OutputTransformation`
  (`•` × length) over a raw `TextFieldState` — the stored value is a normal text state with
  **no copy/paste restriction**.
- **Keyboard / autofill**: iOS defaults `textContentType: .password` + `autocapitalization:
  none` (Keychain autofill). **Android sets no keyboard options at all** — a plain keyboard
  with `ImeAction.Done`: no `KeyboardType.Password`, no autofill semantics, and (because the
  masking is not the platform secure mode) no IME suggestion/learning suppression — flagged.
- **Reveal state ownership**: iOS lets the caller seed `initialPasswordHidden` (then owns it
  internally); Android manages it fully internally AND holds it in `remember` (not
  `rememberSaveable`) — the reveal resets to masked on configuration change, flagged as a
  paper-cut.
- **Anatomy base**: iOS re-implements the field chrome over `DsPortedTextField` with a
  dedicated `SecureCompactField` (no prefix/suffix support); Android reuses the full
  `DsCustomTextField` stack.
- **Loading**: Android swaps the toggle for a `DsLoader` when `isLoading && enabled`; iOS has
  no loader (its `isLoading` environment drives the skeleton; the description hides). Neither
  matches the Figma set, which stages no loading at all.
- **iOS skeleton radius**: `.rectangle(cornerRadius: .s24)` — the family-known drift from the
  box's `s20`.
- **Value announcement**: see the a11y spec — the platforms sit at opposite extremes.

---

## 6. Color tokens

TextField's table applies unchanged (Update 3.1, 2026-02-24 — the same DS 3.0 colour pass as
the family: box `Background/OnWhite`, disabled everything `TextAndIcon/Disabled`).
Password-specific defaults:

| Element | Token | Notes |
|---|---|---|
| Show Password Button icon | `var(--text-and-icon-primary)` | `eye-visibility-on` / `eye-visibility-off` from the DS icons (`icons/outlined/security/`) |
| Masked bullets / revealed text | `var(--text-and-icon-primary)` | The Input text slot, Main Body |
| Label / Helper | `var(--text-and-icon-secondary)` | Default "Password" / hint |
| Helper (error) + icon | `var(--text-and-icon-error)` | With `Border/Error` on the box |

---

## 7. Typography

TextField's table applies unchanged: label Main Body 16/20 ⇄ Compact Body 14/16, the value
(bullets or revealed) Main Body, Helper Compact Body. The A11y-resizing staging scales the
texts in full: label 14 → 18.2 → 21, value 16 → 20.8 → 24 at 100/130/150%.

---

## 8. Spacing

TextField's model applies unchanged (box `s56`/`s20` both max ×130%, s8/s8/s0/s8/s4, `s16`
with slots off, Helper `s8`/`s16`/`s4`). Password-specific values:

| Property | Token | Value @100% | Notes |
|---|---|---|---|
| Show Password Button tap | `var(--sp-s48)` square | 48 | "Tap size: s48x48" — the end-slot geometry |
| Eye icon | `var(--sp-s24)` | 24 | "Icon size: s24x24"; both platforms cap the container at ×130–150 per their slot conventions |
| Slot#1 container / icon | `var(--sp-s40)` / `var(--sp-s24)` | 40 / 24 | When enabled |

Overflow: "Long text is hidden beyond the component's boundary. Truncation is not used." The
Helper "is displayed in full and can occupy multiple lines."

---

## 9. Usage context

### When to use PasswordField

- Any password entry: login, registration, changing a password (pair two fields), unlocking a
  protected action.

### When not to use

- One-time codes from SMS/e-mail — the `InputCode` line (digits are not a secret to mask).
- PIN pads — a numeric keypad pattern, not a text field.
- General text — TextField; phone numbers — PhoneField.
- Do not build strength meters or caps-lock hints into the component — neither platform ships
  one; they are the host screen's content below the field.

### Related components

- [TextField](https://super-dollop-pzmo65r.pages.github.io/text-field.md) — the anatomy and every shared contract.
- [BaseTextLine](https://super-dollop-pzmo65r.pages.github.io/base-text-line.md) — the input core.
- [PhoneField](https://super-dollop-pzmo65r.pages.github.io/phone-field.md) — the sibling assembly on the same anatomy.

---

## 10. Accessibility

Brief summary. Full spec: [`password-field-a11y.md`](https://super-dollop-pzmo65r.pages.github.io/password-field.md). The Figma voiced
previews are the contract:

- The field: *[Password, Helper, Double-tap to enter text. Double-tap and hold to long press]*,
  Role **EditText**.
- The toggle: *[Show password. Button. Double tap to activate]* — and "Hide password" once
  revealed; Role **Button**.
- **The value is never spoken as characters.** The web input is `type="password"` — screen
  readers announce a secure field and suppress the value; revealing switches to plain text at
  the user's explicit request.
- The web box keeps the family's **explicit `for=` → `id`** label pairing (the end slot is a
  real button inside the box).
- The platform implementations sit at opposite extremes (iOS announces nothing of the value —
  the whole subtree is hidden; Android announces the bullet string, leaking the length, and
  the raw value once revealed) — both recorded in the a11y spec with flags.

---

## 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` | `password-field` |
| Coverage unit | yes |
| Tap target model | The field (focus) + the toggle button target |
| Actions | `focus` on the field; `tap` on the toggle |
| Internal targets | `show-password-toggle` — with `data-ds-target` |
| Emits value | **Never the password, masked or revealed, nor its length** — the hardest case of the family rule; at most empty/filled, validation and visibility flags |

Attribute model: `data-ds-state` as TextField; `data-ds-variant` = `hidden` \| `visible` (the
visibility axis).

```html
<div class="text-field password-field" data-ds-component="password-field" data-ds-component-id="login-password"
     data-ds-variant="hidden" data-ds-state="standard-empty">
  <label class="text-field__box" for="login-password-input">
    <span class="text-field__start-text">
      <span class="text-field__label" id="login-password-label">Password</span>
      <span class="base-text-line" data-ds-component="base-text-line">
        <input class="base-text-line__input" id="login-password-input" type="password"
               autocomplete="current-password"
               aria-labelledby="login-password-label" aria-describedby="login-password-desc">
      </span>
    </span>
    <span class="text-field__end-slot">
      <button class="text-field__slot-button password-field__toggle" type="button"
              aria-label="Show password"
              data-ds-action="tap" data-ds-target="show-password-toggle"><!-- eye-visibility-on --></button>
    </span>
  </label>
  <span class="text-field__description" id="login-password-desc">Description</span>
</div>
```

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.1.1 | 2026-11-12 | Status Draft → Ready (owner, 2026-11-12) — the backlog sweep: merged on main, web build shipped, no owner decision open. No contract change; Figma-side flags recorded above stay open on the Figma side. |
| 3.1.0 | 2026-09-29 | Initial spec, aligned to the Figma line `[PasswordField] 3.1` (master `13282:23022` — the outer-margin wrapper hosting the PasswordField set `8216:7548`: State (five values, **no data loading**) × Disabled × Validation × RTL + Slot#1/Description booleans and the show/hide button swap). The TextField anatomy with locked password content: the value masked with `•` bullets by default (Main Body), the Show Password Button fixed in the end slot (s48 tap / s24 icon, `eye-visibility-on` ⇄ `eye-visibility-off`, present in every state incl. empty and disabled), Slot#1 optional and off by default in usage, Helper below, default label "Password", the reveal a persistent toggle (no hold-to-reveal, no auto re-mask). Update 3.1 (2026-02-24): the DS 3.0 colour pass (box `Background/OnWhite`; disabled all-`TextAndIcon/Disabled`). Cross-checked against both shipped builds (iOS `DsSecureTextField`, Android `DsPasswordTextField` 3.1): icons, s48/s24, toggle-persists and always-visible confirmed on both. Recorded divergences: masking mechanism (iOS OS secure mode with copy suppression vs Android display-only `OutputTransformation` over a raw state, no copy restriction); keyboard/autofill (iOS `textContentType .password` vs **Android no password keyboard/autofill semantics at all** — flagged); Android reveal state resets on configuration change (`remember`, flagged); iOS 250 ms compact float vs the family's 400 ms Scale Medium; Android's `isLoading` swaps the toggle for a loader — outside the Figma set (no loading State staged); iOS skeleton radius s24 (family drift); value announcement extremes (iOS hides the whole subtree from VoiceOver — the toggle only a custom action; Android announces the bullets — the length leaks — and the raw value when revealed) — both flagged in the a11y spec. Web build shipped: `type="password"` masking + the toggle on the TextField block, `autocomplete="current-password"`, the native Edge reveal hidden, the explicit `for=`→`id` pairing (the family rule); manifest bound (`password-field`: master + set nodes — both platforms present in the seed). Review pass (same day, spec ~96% / a11y ~90%, no blockers): the contrast pairing corrected to the real web token (`--surface-on-white` — the Figma Update names it Background/OnWhite, the web palette ships it as Surface); the eye SVGs made `aria-hidden` (the buttons carry the labels); the disabled specimen's Description wired via `aria-describedby`; the Android Slot#1 interactive branch documented (the host's own Role.Button node); the stale "not in the web DS yet" sibling references fixed in TextField 3.2.1 / PhoneField 3.1.1. |

---

# PasswordField — Accessibility

Two focus stops: the secure field and the Show Password Button. The Figma voiced previews are
the contract — the field announces *[Password, Helper, Double-tap to enter text. Double-tap and
hold to long press]* (Role EditText), the toggle *[Show password. Button. Double tap to
activate]*. **The value is never spoken as characters while masked** — that is the whole point
of the component; revealing it is the user's explicit, announced action.

Label rule: the toggle's name states the ACTION it will perform — "Show password" while masked,
"Hide password" while revealed (both platforms ship exactly these strings) — never a state-only
name like "eye" or "visibility".

## Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Field | "Password" (the label) + the Helper text | The **rendered** text: the bullet string while masked, the raw value when revealed | Edit text | "Double-tap to enter text. Double-tap and hold to long press" |
| Show Password Button | "Show password" / "Hide password" per state | — | `Role.Button` (a real node, `ds_password_text_field_visibility_icon` test tag) | "Double tap to activate" |
| Slot#1 icon (when on) | Decorative by default — no label, skipped by TalkBack | — | — | — |
| Slot#1 interactive (host-wired) | The host's own label | — | The host's own `Role.Button` node — its own focus stop; the slot itself adds no semantics | "Double tap to activate" |

- **The masked value announces as bullets — the length leaks.** `DsPasswordTextField` does not
  use the `inputContentDescription` override its own base offers for this; TalkBack reads
  `•••••••` (seven items = seven characters). Flagged to the Android owners.
- **The revealed value is announced raw** — expected once the user reveals, but combined with
  the missing password keyboard semantics (no `KeyboardType.Password`, no autofill
  `contentType`) the field is indistinguishable from a plain text field to the system — flagged.
- **Disabled**: the toggle stays visible, non-interactive (`SlotClickable(enabled=false)`),
  everything tints `TextAndIcon/Disabled`.

### Edge states

- **Skeleton**: the whole box; the ambient `DsSkeleton` scope carries the busy semantics — the
  field content is not announced.
- **Error**: the Helper turns error-coloured with the 16 icon; the text is the message channel —
  the colour never carries it alone.
- **Loading** (the extra knob outside the Figma set): the toggle is REPLACED by a loader — the
  reveal action disappears mid-session; recorded as part of the loading-gate divergence.
- **RTL**: the chrome mirrors; announcements unchanged.

---

## iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Field (combined) | The placeholder ("Password") + the description text, comma-joined | **Nothing** — the field chrome is `accessibilityHidden`; neither the value nor its length is ever announced | One combined element (`children: .combine`); the button trait removed | Compact empty+unfocused: "text field, double tap to edit" |
| Show Password Button | "Show password" / "Hide password" | — | **Not a reachable element** — exposed only as a custom accessibility action on the combined element (gated on enabled) | — |
| Slot#1 interactive icon | Its own label | — | A second custom action | — |

- **The privacy extreme**: the masked value and its length are never exposed — but the cost is
  that VoiceOver users get no standard secure-field editing surface and must discover the
  reveal through the actions rotor. Flagged as a UX trade-off, not a defect.
- The system announces the secure mode itself (`isSecureTextEntry`) when the input is reached
  for editing; copy/cut are suppressed by the OS while masked.

### Edge states

- **Skeleton**: `hasDsSkeleton` (radius s24 — the family drift); **Disabled**: the custom
  actions are gated off; **Error / RTL**: as Android — host-owned message, mirrored chrome.

---

## Web preview (reference)

- **The input is `type="password"`** — the platform-correct secure semantic: screen readers
  announce a protected/secure edit field and never echo typed characters; the OS/browser
  suppresses suggestions and offers the password manager (`autocomplete="current-password"`).
  Revealing swaps to `type="text"` — a deliberate, user-invoked exposure.

  ```html
  <label class="text-field__box" for="pw-input">
    <span class="text-field__start-text">
      <span class="text-field__label" id="pw-label">Password</span>
      <span class="base-text-line">
        <input class="base-text-line__input" id="pw-input" type="password"
               autocomplete="current-password"
               aria-labelledby="pw-label" aria-describedby="pw-desc">
      </span>
    </span>
    <span class="text-field__end-slot">
      <button class="text-field__slot-button password-field__toggle" type="button"
              aria-label="Show password"><!-- eye-visibility-on, aria-hidden --></button>
    </span>
  </label>
  <span class="text-field__description" id="pw-desc">Description</span>
  ```

- **The toggle is a real `<button>`** whose `aria-label` swaps "Show password" ⇄ "Hide
  password" with the icon (the platform string convention); the eye SVG itself is
  `aria-hidden="true"`. Focus stays on the button through the swap; the input's caret and value
  survive the `type` switch.
- **The explicit `for=` → `id` pairing** (the family rule): the end slot is a real button
  inside the box — an implicit label association would mis-target it.
- The native Edge reveal control is hidden (`::-ms-reveal`) — one toggle, the DS one.
- **Never `aria-live` on the value** — revealing must not cause the password to be spoken
  unprompted; the announcement channel is the toggle's own label swap.
- **Disabled**: the real `disabled` attribute on both the input and the toggle.
- **Skeleton**: `aria-hidden="true"` on the field; the loading container carries `aria-busy`.

### Colour and contrast

| Pairing | Tokens | Requirement |
|---|---|---|
| Value (bullets or revealed) on the box | `--text-and-icon-primary` on `--surface-on-white` | ≥ 4.5:1 — the TextField measurement holds (≈ 14.7:1 Day) |
| Label / Helper | `--text-and-icon-secondary` | ≥ 4.5:1 — ≈ 5.0:1 Day on OnWhite (the TextField measurement) |
| Eye icon | `--text-and-icon-primary` | ≥ 3:1 (non-text) — same as the value |
| Error border / Helper error | `--border-error` / `--text-and-icon-error` | The family-known red flag: error text ≈ 3.5:1 Day on OnWhite — under the 4.5:1 small-text floor, flagged DS-wide (TextField measurement) |
| Disabled | `--text-and-icon-disabled` | Exempt (disabled) |

---

## Machine contract — `specs/components/password-field/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": "password-field",
  "name": "PasswordField",
  "version": "3.1.1",
  "description": "The password field: the TextField anatomy with locked password content — the value masked by type=\"password\" (bullets), the Show Password Button fixed in the end slot (s48 tap / s24 eye, present in every state), the Helper below. No loader, no clear button, no data-loading State. The root carries BOTH classes — .text-field (every TextField contract) plus .password-field (the password-specific block).",
  "files": {
    "spec": "specs/components/password-field/password-field.md",
    "a11y": "specs/components/password-field/password-field-a11y.md",
    "preview": "src/password-field.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "🕹️ Oymyakon 3.32.3 (components)",
    "fileKey": "7vdl5YkZFDWvh9QvSmydsH",
    "componentSets": {
      "PasswordField": {
        "key": "5086dac2eb5a84ed47f70c4c4c050228d691b9e8"
      }
    },
    "capturedAt": "2026-09-29"
  },
  "root": {
    "class": "password-field",
    "dataDsComponent": "password-field"
  },
  "anatomy": {
    "startSlotIcon": {
      "class": "text-field__start-slot",
      "optional": true,
      "notes": "Slot#1 — \"Only icons from the DS can be placed. By default, it should be disabled\": the s40 container with the s24 icon in TextAndIcon/Primary. The only open slot of the component."
    },
    "maskedInput": {
      "class": "base-text-line__input",
      "notes": "The entered password, masked by default: the web uses type=\"password\" (the platform-correct secure semantic — iOS's isSecureTextEntry analogue; Android masks display-only with • bullets). Main Body, TextAndIcon/Primary; the caret adopts the same colour. autocomplete current-password / new-password engages the password manager."
    },
    "toggle": {
      "class": "password-field__toggle",
      "notes": "The Show Password Button, FIXED in the end slot (both platforms hardwire it — \"do not pass one\"): the .text-field__slot-button geometry, s48 tap / s24 icon, eye-visibility-on while hidden ⇄ eye-visibility-off while visible (icons/outlined/security). Present in every state incl. empty and disabled; aria-label swaps \"Show password\" ⇄ \"Hide password\"."
    },
    "helper": {
      "class": "text-field__description",
      "optional": true,
      "notes": "The TextField Description contract; the error text with the 16 error icon (Update 3.1). Never echo the entered password in the message."
    }
  },
  "axes": {
    "state": {
      "title": "State",
      "type": "enum",
      "values": [
        "standard-empty",
        "active-empty",
        "active-filled",
        "standard-filled",
        "skeleton"
      ],
      "default": "standard-empty",
      "css": {
        "mechanism": "the TextField mechanisms (focus-within = active; .text-field--filled marks content; .text-field--skeleton the whole box)"
      },
      "figma": {
        "kind": "variant-property",
        "property": "State",
        "values": {
          "standard-empty": "rest empty",
          "active-empty": "active empty",
          "active-filled": "active filled",
          "standard-filled": "rest filled",
          "skeleton": "skeleton"
        },
        "notes": "Figma's 'rest' maps to 'standard' per spec-conventions. NO data-loading value — unlike PhoneField, the set stages none; Android's inherited isLoading knob (swaps the toggle for a loader) is outside the Figma contract, recorded."
      }
    },
    "visibility": {
      "title": "Visibility",
      "type": "enum",
      "values": [
        "hidden",
        "visible"
      ],
      "default": "hidden",
      "css": {
        "mechanism": "type=\"password\" ⇄ type=\"text\" + the eye icon and aria-label swap — a runtime toggle, not a modifier class; data-ds-variant carries the value"
      },
      "figma": {
        "kind": "instance-swap",
        "property": "Instance",
        "notes": "Staged via the password-button-show / password-button-hide swap (eye-visibility-on / -off inside). A persistent toggle: no hold-to-reveal, no auto re-mask, no re-mask on focus loss — on either platform or the web."
      }
    },
    "disabled": {
      "title": "Disabled",
      "type": "boolean",
      "default": false,
      "css": {
        "modifier": ".text-field--disabled",
        "mechanism": "plus the input's and the toggle's disabled attribute; the eye stays visible, non-interactive"
      },
      "figma": {
        "kind": "variant-property",
        "property": "Disabled"
      }
    },
    "validation": {
      "title": "Validation",
      "type": "enum",
      "values": [
        "none",
        "error"
      ],
      "default": "none",
      "css": {
        "modifier": ".text-field--error"
      },
      "figma": {
        "kind": "variant-property",
        "property": "Validation"
      }
    },
    "rtl": {
      "title": "RTL",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "dir=\"rtl\" mirrors the chrome — the toggle moves to the start edge; the masked value is symmetric bullets, no forced-LTR line staged"
      },
      "figma": {
        "kind": "variant-property",
        "property": "RTL"
      }
    },
    "startSlot": {
      "title": "Slot#1",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "presence of the start-slot icon"
      },
      "figma": {
        "kind": "boolean-property",
        "property": "🔴StartSlot",
        "notes": "The Figma boolean stages true; the Specification's usage default is OFF (\"By default, it should be disabled\") — the spec default wins."
      }
    },
    "helperSlot": {
      "title": "Helper",
      "type": "boolean",
      "default": true,
      "css": {
        "mechanism": "presence of .text-field__description"
      },
      "figma": {
        "kind": "boolean-property",
        "property": "Description"
      }
    },
    "labelMode": {
      "title": "Label mode",
      "type": "enum",
      "values": [
        "compact",
        "placeholder"
      ],
      "default": "compact",
      "css": {
        "modifier": ".text-field--placeholder-label"
      },
      "figma": {
        "kind": "none",
        "notes": "The TextField contract; the default label text is \"Password\"."
      }
    }
  },
  "analytics": {
    "dataDsComponent": "password-field",
    "action": "focus",
    "valueAttr": "data-ds-state",
    "notes": "Coverage unit. Internal target: show-password-toggle (data-ds-action=tap + data-ds-target). data-ds-variant carries hidden|visible. NEVER emit the password, masked or revealed, nor its length — the hardest case of the family rule; at most empty/filled, validation and visibility flags."
  },
  "rtl": {
    "supported": true,
    "notes": "A Figma variant axis: the chrome (slots, label, Helper) mirrors and the toggle moves to the start edge; the masked value is symmetric bullets, so no forced-LTR line is staged (unlike PhoneField's digits)."
  },
  "constraints": [
    "Every TextField constraint applies (the anatomy, spacing, motion, no-truncation, error persistence).",
    "The end slot is NOT configurable — it always hosts the Show Password Button; Slot#1 is the only open slot and defaults off.",
    "The reveal is a persistent toggle: no hold-to-reveal, no auto re-mask timer, no re-mask on focus loss; the mask ⇄ text swap is instant (no animation).",
    "The toggle names its ACTION — \"Show password\" ⇄ \"Hide password\" — and stays visible in every state incl. empty and disabled.",
    "No data-loading State, no loader, no clear button, no strength meter, no caps-lock hint — meters and hints are the host screen's content.",
    "The box pairs for= with the input's id EXPLICITLY — the end slot is a real button inside the box (the family rule).",
    "Never aria-live near the value: revealing must not speak the password unprompted.",
    "Never emit the password (masked or revealed) or its length through analytics; never echo it in the Helper/error message."
  ]
}
```
