# Elements: Form controls

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from core/dist/elements/api.json. Do not edit: change the source and run the generator.

How to read these tables: see `elements-index.md`. Each element shows its first example; the others are in `references/full/examples-form-controls.md` (kept apart so the group files stay short).

## `pk-calendar`

**Calendar** (Forms). A small month grid to pick or show a date, with marks, minimum and maximum, and full keyboard control.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `value` | string | `""` | The selected day, an ISO date such as 2026-09-19. |
| `month` | string | `""` | An ISO date whose month is shown; defaults to the value or today. |
| `min` | string | `""` | Earliest selectable ISO date. |
| `max` | string | `""` | Latest selectable ISO date. |
| `week-start` (property `weekStart`) | number | `0` | First weekday: 0 Sunday, 1 Monday. |
| `locale` | string | `""` | Language of month and weekday names; defaults to the page language. |
| `marks` | json | `[]` | ISO dates that get a dot. |
| `range` | boolean | `false` | Pick a range of days instead of one: the first click sets the start, the second the end (swapped when earlier), the days between are highlighted and a pending range previews under the pointer or focus. Escape drops a pending start. |
| `start` | string | `""` | Range mode: the first day of the range, an ISO date such as 2026-09-01. |
| `end` | string | `""` | Range mode: the last day of the range, an ISO date such as 2026-09-14. |
| `readonly` | boolean | `false` | Show only; no selection. |
| `disabled` | boolean | `false` | Inert. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-select` | `{ value: string }` | A day was chosen. Cancel to keep the current value. |
| `pk-range-change` | `{ start: string, end: string, valid: boolean }` | Range mode: the second click set the end and the range is complete. The same detail as pk-date-range-picker; valid is always true here because min and max cannot be picked past. |
| `pk-month` | `{ month: string }` | The shown month changed (yyyy-mm). |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Focuses the day that holds the roving focus (the selected or first day of the shown month). |
| `monthGrid(year, month, weekStart)` | Weeks of a month. |
| `dateForKey(iso, key)` | Keyboard movement. |
| `addMonths(iso, n)` | Clamped month step. |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The title row. |
| `prev` | Previous month. |
| `title` | Month and year. |
| `next` | Next month. |
| `grid` | The table. |
| `weekdays` | The weekday header. |
| `days` | The day rows. |
| `status` | A visually hidden polite live region for range mode (Range start set to ..., Range: ... to ...). |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-calendar-cell` | `var(--touch-target)` | Day size: the touch target on a phone, 2rem on desktop. |

**Accessibility.** role=grid with a live month title; one roving focused day; arrows move by a day or a week, Home and End to the month edges, PageUp and PageDown by a month (Shift a year). The selected day has aria-selected, today aria-current=date; out-of-range days are disabled buttons with full-date labels. In range mode the start and end days have aria-selected and their labels add range start, range end or in range; a polite live region announces Range start set to X and Range: X to Y; Enter or Space picks the focused day, arrows move focus and preview a pending range, Escape drops a pending start.

Example: Selected day with marks

```html
<pk-calendar value="2026-09-19" marks='["2026-09-05","2026-09-24"]'></pk-calendar>
```

## `pk-checkbox`

**Checkbox** (Form controls). A checkbox drawn from tokens, with an indeterminate state and select-all groups: a master checkbox mirrors every checkbox of its group.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `name` | string | `""` |  | The form field name. |
| `value` | string | `"on"` |  | The value submitted while checked. |
| `checked` | boolean | `false` |  | Whether the box is checked. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `indeterminate` | boolean | `false` |  | The mixed state (some of a group are checked). |
| `label` | string | `""` |  | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `description` | string | `""` |  | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` |  | Blocks interaction; also set by a disabled fieldset. |
| `required` | boolean | `false` |  | The form cannot be submitted while the value is empty. |
| `invalid` | boolean | `false` |  | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |
| `group` | string | `""` |  | Puts this checkbox in a select-all group of that name. |
| `master` | string | `""` |  | Makes this the select-all checkbox of the group of that name; it shows checked, unchecked or indeterminate as its members change. |
| `size` | enum | `"md"` | `md` `sm` | sm is the compact checkbox of a toolbar: smaller label text. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The label. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-change` | `{ checked: boolean }` | The user changed the value (same moment as change; carries it). |

**Methods**

| Method | Description |
|---|---|
| `toggle()` | Toggles as a click does. |
| `focus()` | Focuses the box. |

**CSS parts**

| Part | Description |
|---|---|
| `control` | The label wrapper (the whole row is the tap target). |
| `input` | The inner checkbox. |
| `label` | The label text. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-checkbox-border` | `var(--color-input-border)` | Border colour of the box. |
| `--pk-checkbox-size` | `1.15rem` | Box size. |
| `--pk-checkbox-font` | `var(--text-read)` | Label font size. |
| `--pk-checkbox-gap` | `var(--space-2)` | Space between the box and its label. |
| `--pk-checkbox-color` | `var(--color-text)` | Label colour. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the other pk-checkbox elements of the same group in the document | `checked`, `indeterminate` | A master checkbox (master="name") sets every enabled member (group="name") on change, and a member re-syncs the master from the members. Only with the group attributes. Not undone on disconnect. |

**Accessibility.** A native checkbox inside its label, so it is named by its text and toggled by Space; indeterminate is announced as mixed; invalid sets aria-invalid and a red border. Rows are 44px tall on a phone.

Example: States

```html
<pk-checkbox checked>Track inventory</pk-checkbox>
<pk-checkbox>Include archived</pk-checkbox>
<pk-checkbox indeterminate>Some variants selected</pk-checkbox>
<pk-checkbox disabled>Disabled</pk-checkbox>
```

## `pk-colour-input`

**Colour input** (Form controls). The native colour picker beside an editable hex field that stay in step; the field accepts #abc, abc and #aabbcc and is normalised to #rrggbb.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | The form field name. |
| `value` | string | `""` | The colour as #rrggbb. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `label` | string | `""` | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `show-label` (property `showLabel`) | boolean | `false` | Shows label as visible text above the field (linked to it), for use without a pk-field. |
| `description` | string | `""` | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` | Blocks interaction; also set by a disabled fieldset. |
| `required` | boolean | `false` | The form cannot be submitted while the value is empty. |
| `invalid` | boolean | `false` | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-colour` | `{ value: string }` | A valid colour was picked or typed (lowercase #rrggbb). |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Focuses the hex field. |

**CSS parts**

| Part | Description |
|---|---|
| `label` | The visible label shown by showLabel. |
| `box` | The row of swatch and hex field. |
| `swatch` | The native colour picker. |
| `control` | The hex text field. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-bg` | `var(--color-input)` | Background of the field. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |

**Accessibility.** Both inputs are labelled; text that is not a colour sets aria-invalid and a validity message; the picker is the platform's own, so it is keyboard and screen-reader accessible everywhere. The swatch is 44px on a phone.

Example: Visible label

```html
<pk-colour-input label="Accent colour" show-label value="#4a90e2"></pk-colour-input>
```

## `pk-combobox`

**Combobox** (Form controls). An autocomplete (type to filter a list) or, with mode="select", a custom select. ARIA combobox with the highlight tracked by aria-activedescendant, so focus stays on the control. Its options are its own option children. For a server search set the search(query, { signal }) property: it returns a promise of [{ value, label }] that becomes the list (client filter off); a newer query aborts the signal and late answers are dropped.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `mode` | enum | `"autocomplete"` | `autocomplete` `select` | autocomplete filters as you type; select is a button that opens the list (type a letter to jump). Prefer the native pk-select on a phone. |
| `name` | string | `""` |  | The form field name. |
| `value` | string | `""` |  | The current value; what the form submits. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `placeholder` | string | `""` |  | Hint shown while empty. |
| `label` | string | `""` |  | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `description` | string | `""` |  | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` |  | Blocks interaction; also set by a disabled fieldset. |
| `required` | boolean | `false` |  | The form cannot be submitted while the value is empty. |
| `invalid` | boolean | `false` |  | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |
| `free` | boolean | `false` |  | Autocomplete: keep typed text that is not an option, and use it as the value. |
| `clearable` | boolean | `false` |  | Autocomplete: shows a clear button while a value is chosen. It sets the value to empty, raises change and pk-combo-select (value "", label ""), and puts focus back on the input. For an optional picked value. |
| `filtering` | enum | `"client"` | `client` `off` | client also filters the options here; off leaves filtering to the host, which listens for pk-combo-query and replaces the options (or sets the search callback, which turns the client filter off by itself). |
| `debounce` | number | `0` |  | Milliseconds to wait after typing before pk-combo-query fires, with the latest text only; 0 fires on every keystroke. The client filter is not delayed. For a host that fetches options from the query. |
| `open` | boolean | `false` |  | Whether the list is open. |

**Slots**

| Slot | Description |
|---|---|
| (default) | option elements (value, disabled and text). |
| `empty` | The message shown when nothing matches. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-combo-select` | `{ value: string, label: string }` | An option was chosen. |
| `pk-combo-query` | `{ query: string }` | The user typed: the text so far, so a host can fetch more options (with filtering="off" it also replaces the client filter). |
| `pk-combo-toggle` | `{ open: bool }` | The list opened or closed by itself (typing, a click, a key, focus leaving); open already has the new value. Not raised for a change the host made. |

**Methods**

| Method | Description |
|---|---|
| `show()` | Opens the list. |
| `hide()` | Closes the list. |
| `focus()` | Focuses the control. |

**CSS parts**

| Part | Description |
|---|---|
| `box` | The bordered field. |
| `control` | The input (autocomplete mode). |
| `trigger` | The button (select mode). |
| `clear` | The clear button (clearable). |
| `chevron` | The arrow (select mode). |
| `popup` | The list panel. |
| `empty` | The no-match message. |
| `option` | One option (dynamic). |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-bg` | `var(--color-input)` | Background of the field. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |

**Accessibility.** ARIA 1.2 combobox: role=combobox, aria-expanded, aria-controls and aria-activedescendant (focus stays on the input or button); Arrow keys open and move, Home and End jump (select mode), Enter picks, Escape closes, Tab closes, letters jump in select mode. The chosen option carries aria-selected and a check mark. Options are 44px tall on a phone; the list opens upward when there is no room below.

Example: Autocomplete

```html
<pk-combobox label="Variant" placeholder="Type to search"><option value="AC-1001">AC-1001 - Widget 1</option><option value="AC-1002">AC-1002 - Widget 2</option><option value="AC-1003">AC-1003 - Widget 3</option></pk-combobox>
```

## `pk-date-range-picker`

**Date range picker** (Form controls). A start and an end date with quick ranges (last 7 days, this month), a minimum and maximum, and an inline error when the end is before the start. Built on native date fields, so keyboard, touch and the platform picker come with it.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | The form field name. The submitted value is the ISO interval start/end, for example 2026-09-01/2026-09-07, or empty when neither date is set. |
| `start` | string | `""` | The first day, an ISO date such as 2026-09-01. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it. A browser state restore also sets it silently. |
| `end` | string | `""` | The last day, an ISO date (inclusive). Same reset and restore behaviour as start. |
| `min` | string | `""` | Earliest allowed ISO date. A date before it makes the field invalid and disables a quick range that lies wholly before it. |
| `max` | string | `""` | Latest allowed ISO date. |
| `presets` | string | `"today,7d,30d,month"` | The quick ranges to offer, comma separated, in order: today, 7d, 30d, 90d, month, lastmonth, quarter, lastquarter, year, lastyear, thismonth, thisquarter, thisyear (the whole period, to its last day). Empty for none. A range is cut to min and max. Keys of presetItems may be named here to place them; the ones not named follow. |
| `preset-items` (property `presetItems`) | json | `[]` | The caller's own named presets as data: [{ key, label, start, end }] (ISO dates), for a period vocabulary of your own. They are quick ranges like the built-in ones (cut to min and max, pressed when the range matches) and pk-range-change reports the matching key in detail.preset, so a named period can be stored and re-resolved later. |
| `calendar` | boolean | `false` | Adds a calendar button after the end field that opens a pk-calendar in range mode in a pk-popover. The first click sets the start, the second the end and closes it; the fields stay, so the calendar is an addition and the native inputs are the fallback. Focus moves into the calendar on open, and Escape closes it and returns focus to the button. |
| `label` | string | `""` | The accessible name of the group (aria-label). pk-field fills it from its own label when this is empty. |
| `start-label` (property `startLabel`) | string | `""` | The accessible name of the start field; "Start date" when empty. Set it to localise. |
| `end-label` (property `endLabel`) | string | `""` | The accessible name of the end field; "End date" when empty. |
| `description` | string | `""` | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` | Blocks interaction; also set by a disabled fieldset. |
| `readonly` | boolean | `false` | Shows the range without letting the user change it. |
| `required` | boolean | `false` | The form cannot be submitted until both dates are set. |
| `invalid` | boolean | `false` | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). The element also shows its own error when the end is before the start or a date is outside min and max. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | A field changed while the user typed (native, composed: bubbles out of the shadow root). Read event.target.start and event.target.end. |
| `change` | `native Event` | The user committed a date or a quick range (re-dispatched from the inner control, composed). |
| `pk-range-change` | `{ start: string, end: string, valid: boolean, preset: string }` | The range changed by the user (a date typed or picked, a quick range, clear). valid is false when the end is before the start or a date is outside min and max; the values are still reported so the host can show its own message. preset is the key of the quick range the range equals (built in or from presetItems), else empty. |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Focuses the start field. |
| `clear()` | Empties both dates and raises pk-range-change. |

**CSS parts**

| Part | Description |
|---|---|
| `group` | The wrapper, role=group. |
| `fields` | The row of the two date fields. |
| `start` | The start date field. |
| `separator` | The word between the fields. |
| `end` | The end date field. |
| `presets` | The row of quick-range buttons. |
| `error` | The inline error text (role=alert), hidden when the range is fine. |
| `popover` | The calendar popover (a pk-popover), present with the calendar attribute. |
| `opener` | The calendar button that opens the popover. |
| `calendar` | The range pk-calendar inside the popover. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-bg` | `var(--color-input)` | Background of the fields. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |

**Accessibility.** A role=group named by label with two native date inputs named Start date and End date (segments move with the arrow keys, the platform picker opens from the field). Quick ranges are buttons in a named group with aria-pressed on the one that matches the current range and disabled when they fall outside min and max. An invalid range sets aria-invalid on both fields and shows a role=alert message tied to them with aria-describedby. With the calendar attribute the button carries the popover disclosure (aria-expanded, aria-haspopup=dialog), focus moves to the calendar day on open and returns to the button on Escape or after the range is chosen. On a phone every control is at least 44px tall.

Example: Quick ranges

```html
<pk-date-range-picker label="Reporting period" start="2026-09-01" end="2026-09-14"></pk-date-range-picker>
```

## `pk-dropzone`

**Dropzone** (Form controls). A drop target over a real file input: drag files in or click, with type, size and count checks, a list of accepted files with Remove, and a reason for each rejection. Files are submitted with the form.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | The form field name. |
| `accept` | string | `""` | Accepted types: extensions, MIME types or type/* (like the accept attribute). |
| `multiple` | boolean | `false` | Allows several files. |
| `max-size` (property `maxSize`) | string | `""` | Largest file: 500KB, 5MB, 1GB or bytes. |
| `max-files` (property `maxFiles`) | number | `0` | Most files (multiple); 0 means no limit. |
| `label` | string | `""` | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `browse-label` (property `browseLabel`) | string | `""` | Shows a real button with this text inside the zone (for example "Choose files") that opens the file picker. It becomes the zone's keyboard stop, and the file input stays only as the click target. Empty (the default) draws no button: the whole zone opens the picker. |
| `description` | string | `""` | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` | Blocks interaction; also set by a disabled fieldset. |
| `required` | boolean | `false` | The form cannot be submitted while the value is empty. |
| `invalid` | boolean | `false` | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |

**Slots**

| Slot | Description |
|---|---|
| (default) | The zone title. |
| `hint` | A hint under the title (types and limits). |
| `input` | An existing file input (a Blazor InputFile) that keeps doing the reading: the zone then only draws the target, and dropped files are placed in that input (one file unless it is multiple). Put slot="input" on the input itself (in Blazor: <InputFile slot="input" OnChange=... />) so it covers the whole zone. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `change` | `native Event` | The file list changed. |
| `pk-files` | `{ count: number, rejectedCount: number, files: File[], rejected: { file: File, reason: string }[] }` | Files were picked or dropped: those kept and those refused with a reason. |

**Methods**

| Method | Description |
|---|---|
| `pick()` | Opens the file picker, for a button of the host's own, so no hidden native input is needed. Call it from a click or key handler (the browser requires a user gesture); it does nothing while disabled. With an input in the input slot it opens that input's picker. |
| `clear()` | Removes every file. |
| `focus()` | Focuses the file input. |

**CSS parts**

| Part | Description |
|---|---|
| `zone` | The dashed drop target. |
| `control` | The real file input, invisible over the zone. |
| `title` | The zone title. |
| `hint` | The zone hint. |
| `browse` | The button that opens the file picker (only when browse-label is set). |
| `list` | The list of files. |
| `item` | One file row (dynamic). |
| `item-name` | A file name (dynamic). |
| `item-note` | A file size or rejection reason (dynamic). |
| `item-remove` | A Remove button (dynamic). |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-border` | `var(--color-input-border)` | Dashed border colour. |

**Accessibility.** A real focusable file input covers the zone, so the keyboard opens the picker and the zone shows the focus ring; the list is a polite live region; rejections are text (with the reason), not just red; each Remove button names its file. The whole zone is the tap target on a phone. With browse-label, a real button is the one keyboard stop (its name is its text followed by the zone label) and the file input leaves the tab order and the accessibility tree; the button is 44px tall on a touch screen and on a phone-width page.

Example: Drop zone

```html
<pk-dropzone label="Files to import" accept=".csv,text/csv" multiple max-size="5MB" max-files="3">Drop files here or click to choose<span slot="hint">CSV, up to 5 MB each, 3 files at most</span></pk-dropzone>
```

## `pk-input`

**Input** (Form controls). The single-line control: text, email, url, tel, search, password, number and the date and time types, with an optional clear button, password reveal, number stepper, money format, prefix and suffix, and a floating label.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `type` | enum | `"text"` | `text` `email` `url` `tel` `search` `password` `number` `date` `time` `datetime-local` `month` `week` | The kind of value. Date and time types open the platform picker. |
| `name` | string | `""` |  | The form field name. |
| `value` | string | `""` |  | The current value; what the form submits. A form reset or browser restore sets it silently, as a native control does (pk-form raises pk-reset after a reset). |
| `placeholder` | string | `""` |  | Hint shown while empty (ignored by a floating label). |
| `label` | string | `""` |  | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `show-label` (property `showLabel`) | boolean | `false` |  | Shows label as visible text above the field (linked to it), for use without a pk-field. Ignored by a floating label. |
| `description` | string | `""` |  | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` |  | Blocks interaction; also set by a disabled fieldset. |
| `readonly` | boolean | `false` |  | The value can be read and selected but not edited. |
| `required` | boolean | `false` |  | The form cannot be submitted while the value is empty. |
| `invalid` | boolean | `false` |  | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |
| `warning` | boolean | `false` |  | Shows the warning state (a value the user may want to check). |
| `valid` | boolean | `false` |  | Shows the valid state. |
| `min` | string | `""` |  | Minimum value (number and date types). |
| `max` | string | `""` |  | Maximum value (number and date types). |
| `step` | string | `""` |  | Step for number and date types; also the stepper increment. |
| `minlength` | string | `""` |  | Minimum text length. |
| `maxlength` | string | `""` |  | Maximum text length. |
| `pattern` | string | `""` |  | A regular expression the whole value must match. |
| `autocomplete` | string | `""` |  | The autofill token, for example current-password, new-password or one-time-code. |
| `inputmode` | string | `""` |  | The virtual keyboard to request (numeric, decimal, email, ...). |
| `size` | enum | `"md"` | `sm` `md` `lg` | Control height. |
| `font` | string | `""` |  | Font family of the text: sans or mono (for code, queries, ids). Default inherit. |
| `width` | string | `""` |  | Width: xs to 2xl, or a CSS length. |
| `clearable` | boolean | `false` |  | Shows a clear button while there is a value. |
| `reveal` | boolean | `false` |  | Password only: a show and hide button. |
| `copyable` | boolean | `false` |  | Shows a copy-to-clipboard button next to the value; disabled while empty and gives brief "Copied" feedback. Fails silently (logged, not shown) on an insecure origin or a denied permission. |
| `stepper` | boolean | `false` |  | Number only: minus and plus buttons that respect min, max and step. |
| `format` | enum | `"none"` | `none` `money` | money formats the value as an amount (1,234.50) when the field is left and shows the plain number while editing; value stays the plain number. |
| `decimals` | number | `2` |  | Decimal places for the money format. |
| `currency` | string | `"USD"` |  | ISO currency code whose symbol leads a money field. |
| `locale` | string | `"en-US"` |  | Locale for money formatting. |
| `floating` | boolean | `false` |  | Shows label inside the field until it has focus or a value, then floats it to the top edge. |
| `listbox` | json | `null` |  | Property only: the listbox element this input drives, making it a combobox (the option marked aria-selected='true' is the aria-activedescendant). It may sit in the host's shadow tree. Call requestUpdate() after changing it. Needs Chrome 135, Safari 16.4 or Firefox 136. |
| `select-on-focus` (property `selectOnFocus`) | boolean | `false` |  | Selects the whole value when the control gains focus (keyboard, tap or click), so typing replaces it. Useful for number inputs on a phone. |
| `autohide` | number | `0` |  | Password reveal: seconds until the value is masked again; 0 keeps it revealed. |
| `debounce` | number | `0` |  | Search only: milliseconds to wait after typing before pk-search fires; 0 fires on every change. |
| `search-on` (property `searchOn`) | enum | `"change"` | `change` `enter` | Search only: when pk-search fires. change: while typing (after debounce) and at once on Enter, clear and when the box becomes empty. enter: only on Enter and clear (the clear button, the browser's own clear button, Escape, or deleting every character), never while typing, for a lookup or barcode box. |

**Slots**

| Slot | Description |
|---|---|
| `prefix` | A leading add-on: a currency sign, an icon, a unit. |
| `suffix` | A trailing add-on: a unit, a domain, a button. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-value-change` | `{ value: string }` | The user committed a new value (same moment as change; carries the value). |
| `pk-search` | `{ value: string }` | Search type: the search text in detail.value (Blazor PkSearchEventArgs.Value; Query is pk-table-filters' field), debounced while typing, at once on Enter and clear; with search-on="enter" only on Enter and clear. |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Moves focus to the inner input. |

**CSS parts**

| Part | Description |
|---|---|
| `box` | The bordered field that holds the control and its add-ons. |
| `label` | The visible label shown by showLabel. |
| `control` | The inner input element. |
| `search-icon` | The magnifier of a search field. |
| `currency` | The currency symbol of a money field. |
| `floating-label` | The floating label. |
| `clear` | The clear button. |
| `reveal` | The password reveal button. |
| `copy` | The copy-to-clipboard button. |
| `step-down` | The stepper minus button. |
| `step-up` | The stepper plus button. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-bg` | `var(--color-input)` | Background of the field. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |

**Accessibility.** A native input lives in the shadow root, named by label (aria-label) and described by description (aria-description); invalid sets aria-invalid, and the state is never colour alone (the ring is thicker). Buttons are labelled and reveal reports aria-pressed. The copy button is disabled while the value is empty and its aria-label becomes "Copied" briefly after a copy; a clipboard failure is logged, never shown as success. Money and stepper fields keep the number in value while showing formatted text. Text is 16px and the control 44px tall on a phone. With listbox set the input is a combobox (role, aria-expanded, aria-controls, aria-activedescendant; Chrome 135, Safari 16.4 or Firefox 136).

Example: Text, states and sizes

```html
<pk-input label="Product title" value="Widget 1"></pk-input>
<pk-input label="Read only" value="AC-1001" readonly></pk-input>
<pk-input label="Invalid" value="bad sku" invalid></pk-input>
<pk-input label="Small" size="sm"></pk-input>
```

## `pk-lookup-picker`

**Lookup picker** (Form controls). A select-style field for a long list (customers, products, users) that does not fit pk-combobox: its popup holds a searchable, pageable pk-data-table, so the reader tells rows apart by columns. Set the load(query) callback as a property, exactly as pk-data-table's: it receives { page, pageSize, sort, sortDir, search, filters } and { signal }, and returns (or resolves to) { rows, total }; loading, error and empty states come from the table. Picking a row sets value to its rowKey field and closes the popup. The field shows the option's label: its labelKey field when picked in this session, else selectedLabels[value], else what the resolve(keys) callback property returns for it ({ key: label } or [{ key, label }]), so a stored value shows its name even when its row is on no loaded page. The popup is built on the first open: a closed picker loads nothing but itself.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | The form field name. |
| `multiple` | boolean | `false` | Several rows can be chosen: the popup table gets a checkbox column (the selection survives paging and searching; there is no select-all across pages) and stays open while ticking; the chosen rows show as removable chips under the field. The form submits one entry per key under name. |
| `values` | json | `[]` | The keys of the chosen rows when multiple is set, as an array of strings (any text, commas included); what the form submits. A form reset sets it back to its initial value silently. Chip labels come from the rows seen, selectedLabels or resolve(keys), which is asked once for all keys with no label. |
| `max` | number | `0` | The most rows a multiple picker accepts (0: no limit). A tick past it is undone and announced. |
| `value` | string | `""` | The key (rowKey field) of the chosen row; what the form submits. A form reset sets it back to its initial value silently, as a native control does. |
| `columns` | json | `[]` | The popup table's column definitions ({ key, label, type?, align?, sortable?, hidePhone? }[]). Required for the table to show anything. |
| `page-size` (property `pageSize`) | number | `25` | The popup table's page size (initial only). |
| `search-label` (property `searchLabel`) | string | `""` | The placeholder and accessible name of the popup's search box (default "Search <label>"). |
| `search-debounce` (property `searchDebounce`) | number | `250` | Milliseconds the popup's search box waits after the last keystroke before searching. |
| `filters` | json | `[]` | The popup table's filter fields, as pk-data-table's `filters`: [{ key, label, type?, options? }]. |
| `empty` | json | `null` | { heading?, description? } the popup shows when a page has zero rows and nothing is searched (pk-data-table's `empty`). |
| `no-results` (property `noResults`) | json | `null` | { heading?, description? } shown instead while a search or filter is active (pk-data-table's `noResults`). |
| `load-error` (property `loadError`) | string | `""` | The heading of the popup's failed-load message (pk-data-table's `loadError`). |
| `row-key` (property `rowKey`) | string | `"id"` | The row field that identifies a row: what value holds. |
| `label-key` (property `labelKey`) | string | `"name"` | The row field shown in the field once a row is picked (the rowKey field when the row has none). |
| `selected-labels` (property `selectedLabels`) | json | `{}` | { key: label } for values whose row is not on a loaded page, so the field can show a stored value's name without asking the server. |
| `placeholder` | string | `""` | Hint shown while empty. |
| `label` | string | `""` | The accessible name of the field and of the popup. pk-field fills it from its own label when this is empty. |
| `description` | string | `""` | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` | Blocks interaction; also set by a disabled fieldset. |
| `readonly` | boolean | `false` | Shows the value but cannot open the popup. |
| `required` | boolean | `false` | The form cannot be submitted while no row is chosen. |
| `invalid` | boolean | `false` | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |
| `open` | boolean | `false` | Whether the popup is open. |

**Slots**

| Slot | Description |
|---|---|
| `empty` | Content for the popup's empty state (no rows, nothing searched); replaces the empty prop. Read when the popup first opens. |
| `no-results` | Content for the popup when a search or filter matches nothing; replaces the noResults prop. Read when the popup first opens. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed). Read event.target.value. |
| `change` | `native Event` | The reader picked a row (native, composed). |
| `pk-lookup-select` | `{ value: string, label: string, row: object }` | A row was picked: its key, its label and the row object from load(). |
| `pk-values-change` | `{ values: string[], labels: string[] }` | The multiple selection changed (a row ticked or unticked, a chip removed): the keys and their labels. input and change are raised with it. |
| `pk-lookup-toggle` | `{ open: bool }` | The popup opened or closed by itself (a click, a key, a pick, Escape, an outside press, focus leaving); open already has the new value. Not raised for a change the host made. |

**Methods**

| Method | Description |
|---|---|
| `show()` | Opens the popup. |
| `hide()` | Closes the popup. |
| `focus()` | Focuses the field. |

**CSS parts**

| Part | Description |
|---|---|
| `popover` | The pk-popover that holds the popup (placement, outside press, Escape and focus return are its). |
| `control` | The pk-button that shows the chosen label (or the placeholder) and opens the popup. |
| `chevron` | The arrow at the end of the field (turns over while the popup is open). |
| `chips` | The list of chips (pk-tag, removable) under a multiple picker. |
| `text` | The label inside the field. |
| `table` | The pk-data-table in the popup, created on the first open. |
| `status` | The polite live region that announces loading and the result count. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-lookup-picker-bg` | `var(--color-input)` | Background of the field. |
| `--pk-control-bg` | `var(--color-input)` | Background of the field. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the pk-data-table in the popup | `clickable`, `selected` | The picker creates the table in its own shadow tree on the first open and drives it (rows clickable or, for multiple, selectable with page-only selection; keyed; its columns, page size and a wrapped load). Only clickable and selected are writes the check can see; selected follows values. |
| the text span of the field | `data-placeholder` | Marks the text span (created by the picker in its own shadow tree) while it shows the placeholder, so it is styled muted. |

**Accessibility.** The field is a real button (pk-button) that the popover marks aria-haspopup="dialog" and aria-expanded, named "<label>: <chosen label or placeholder>"; the popup is a labelled dialog holding the table's search box and rows, so focus really moves into it (a combobox with aria-activedescendant cannot reach rows inside the table's shadow roots; this is the disclosure-plus-dialog pattern, not role=combobox). Down or Up on the field, Enter or Space open it and focus lands in the search box; Down there enters the rows, Up, Down, Home and End walk them, Enter or Space picks the focused row and closes; Escape closes and returns focus to the field; Tab moves on through the pager and out of the picker, which closes it. A polite status announces loading and the number of results. The table's own loading, error and empty states keep their roles. The field is at least 44px tall on a phone (the table's own search box and rows are the table's to size). Multiple: the chips are a labelled list (role=list, each pk-tag role=listitem) whose remove buttons are real buttons (pk-tag names each "Remove <key>"); removing one returns focus to the field and a status announces it. The table rows have checkboxes named for their row, and a tick past max is undone with "Limit of N reached" announced.

Example: Customer lookup (set load from script: an inline script cannot run under this site's CSP)

```html
<pk-lookup-picker label="Customer" placeholder="Choose a customer" label-key="name" columns='[{"key":"name","label":"Name"},{"key":"city","label":"City"}]' page-size="8"></pk-lookup-picker>
```

## `pk-markdown-editor`

**Markdown editor** (Form controls). A field for writing Markdown: a real textarea whose value is the Markdown text, a formatting toolbar (bold, italic, heading, quote, bulleted and numbered lists, link) and a Write / Preview switch. The preview is built from the parsed text with DOM calls only: raw HTML is shown as text and a link must be http, https, mailto or a relative address. Form-associated like pk-textarea. The Markdown subset is paragraphs, bold, italic, code, fenced code, headings 1 to 3, quotes, lists and links; the same parser and renderer are in js/markdown/ for showing stored text elsewhere (never write the value into markup yourself).

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `name` | string | `""` |  | The form field name. |
| `value` | string | `""` |  | The Markdown text; what the form submits. A form reset sets it back to its initial value without raising pk-change, as a native control does. |
| `placeholder` | string | `""` |  | Hint shown while empty. |
| `label` | string | `""` |  | The accessible name of the writing area. pk-field fills it from its own label when this is empty. |
| `description` | string | `""` |  | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` |  | Blocks editing and commands and takes the toolbar out of the tab order. |
| `readonly` | boolean | `false` |  | The text can be read, selected and previewed, but not edited; the toolbar does nothing. |
| `required` | boolean | `false` |  | The form cannot submit while the text is empty. |
| `maxlength` | string | `""` |  | Maximum text length; a command that would exceed it does nothing. |
| `rows` | number | `6` |  | Visible rows of the writing area. |
| `mode` | enum | `"write"` | `write` `preview` | Which pane shows: write or preview. The Write / Preview tabs change it. |
| `layout` | enum | `"tabs"` | `tabs` `split` | tabs: one pane at a time. split: write and preview side by side while the field is at least 640px wide (the tabs return when narrower). |
| `toolbar` | string | `"bold italic heading quote ul ol link"` |  | The toolbar commands, space separated, in order: bold, italic, heading, quote, ul, ol, link. Enter in a list continues it (and ends it on an empty item); pasting an http, https or mailto address over selected text makes it a link. Ctrl/Cmd+B, I and K work in the textarea for the commands that are listed. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The text changed by typing (native, composed). |
| `change` | `native Event` | The user left the field after editing (native). |
| `pk-change` | `{ value: string }` | The user committed a new value: the field lost focus after typing, or a toolbar command or shortcut changed the text. |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Moves focus to the textarea. |
| `select()` | Selects the text. |

**CSS parts**

| Part | Description |
|---|---|
| `head` | The tab strip and toolbar row. |
| `tabs` | The Write / Preview tabs. |
| `bar` | The toolbar wrapper. |
| `commands` | The formatting buttons (role toolbar, one tab stop, arrow keys move). |
| `linkbar` | The link address row, shown by the link command. |
| `url` | The link address input. |
| `link-ok` | The button that adds the link. |
| `link-cancel` | The button that closes the link row. |
| `error` | The inline error (a refused link). |
| `panes` | The write and preview panes. |
| `write` | The writing pane. |
| `control` | The textarea. |
| `preview` | The rendered preview (a named region). |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-bg` | `var(--color-input)` | Background of the panes. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the pk-button commands in its toolbar | `tabindex` | The editor builds the formatting buttons in its own shadow tree and keeps one of them in the tab order (roving tabindex on the real button inside each). |

**Accessibility.** The writing area is a native textarea named by label. The formatting buttons form one toolbar with a name, one Tab stop and Left/Right, Home and End to move; Escape returns to the textarea. Ctrl/Cmd+B, I and K are announced with aria-keyshortcuts and do nothing during IME composition. The preview is a named region.

Example: Basic

```html
<pk-markdown-editor label="Description" name="description" value="Write **bold** or *italic* text."></pk-markdown-editor>
```

## `pk-otp-input`

**OTP input** (Form controls). A one-time code or PIN as a row of cells: typing advances, Backspace steps back, a pasted or autofilled code spreads across the cells, and completion is an event.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `name` | string | `""` |  | The form field name. |
| `value` | string | `""` |  | The code typed so far. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `label` | string | `""` |  | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `description` | string | `""` |  | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` |  | Blocks interaction; also set by a disabled fieldset. |
| `required` | boolean | `false` |  | The form cannot be submitted while the value is empty. |
| `invalid` | boolean | `false` |  | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |
| `length` | number | `6` |  | Number of cells. |
| `type` | enum | `"numeric"` | `numeric` `alnum` `any` | The alphabet: digits, letters and digits, or anything. |
| `separator-at` (property `separatorAt`) | number | `0` |  | Puts a dash after this many cells (0 for none). |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-otp-change` | `{ value: string }` | The code changed. |
| `pk-otp-complete` | `{ value: string }` | Every cell is filled. |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Focuses the first empty cell. |
| `clear()` | Empties the code. |

**CSS parts**

| Part | Description |
|---|---|
| `group` | The row of cells. |
| `cell` | One cell (dynamic). |
| `separator` | The dash (dynamic). |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-bg` | `var(--color-input)` | Background of the field. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |

**Accessibility.** A role=group named by label; every cell is named "Digit n of N"; the first has one-time-code autocomplete so the OS can fill an SMS code; arrow keys, Home and End move between cells; numeric cells open the numeric keypad. Cells are 44px wide on a phone.

Example: Six digits with a dash

```html
<pk-otp-input label="Verification code" length="6" separator-at="3"></pk-otp-input>
```

## `pk-property-grid`

**Property grid** (Form controls). A property inspector: fields in collapsible groups (text, number, unit, range, colour, select, switch), each a labelled real control, with per-property validation and a pk-property-change event. Uses the same field vocabulary as pk-settings-page.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `config` | json | `{}` | { groups: [{ heading, collapsed?, fields: [{ key, type: 'text'\|'number'\|'unit'\|'range'\|'color'\|'select'\|'switch', label, required?, disabled?, hidden?, visibleWhen?, options?, units?, min?, max?, step? }] }] }. A unit property is a number with a unit picker: units is a list (default ['px']), its value is { value, unit } and min/max apply to the number (the message names the unit). A select's options are [{ value, label? }] or plain strings; range is a pk-range slider with its value shown, color is a #rrggbb text field with a swatch. visibleWhen is { key, equals } or { key, in: [...] } against the current values and hides the property (and skips its validation) when false. Labels sit beside the value once the grid is 480px wide, above it otherwise. |
| `values` | json | `{}` | The property values keyed by field key; set again to update the controls (for example when the selected object changes). |
| `errors` | json | `{}` | Host-side validation messages keyed by field key (for example from a server); shown under the property and marking it invalid. Built-in rules (required, number min/max) apply as well. |
| `state` | json | `{}` | Live per-property state keyed by field key: { key: { disabled?, hidden? } }. Set again to enable, disable, show or hide a property without rebuilding the grid; hidden here overrides visibleWhen. |
| `disabled` | boolean | `false` | Disables every property (a field's own disabled: true stays disabled either way). |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-property-change` | `{ key: string, value: any, values: object, valid: boolean }` | The user committed a new value for one property; carries every current value and whether all properties are valid. |

**Methods**

| Method | Description |
|---|---|
| `setValue(key, value)` | Sets one property's value without rebuilding the grid, so focus, scroll position and open groups stay; validation and visibleWhen are re-evaluated. |

**CSS parts**

| Part | Description |
|---|---|
| `empty` | A placeholder message shown while config.groups is empty. |
| `groups` | The pk-accordion holding one pk-accordion-item per group. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| unit | `label` | The unit editor's picker is a pk-select the grid created in its own shadow tree; it is named after its property. |
| r.unit | `disabled` | The unit picker is disabled together with its number input (state, disabled). |
| swatch | `slot` | The colour editor's swatch is created by the grid itself inside its own shadow tree and slotted into the pk-input prefix. |
| r.row | `hidden` | A property row (a pk-field or switch the grid created in its own shadow tree) is hidden by visibleWhen or state. |

**Accessibility.** Each property is a labelled real control (pk-input, pk-select, pk-switch), so Tab, arrow keys and Space work as in those elements (a unit property's picker is its own labelled select, named "<label> unit"); groups are pk-accordion-item disclosures (Enter or Space toggles). An invalid property sets aria-invalid and shows its message as a pk-alert next to it, so an error is never colour alone. Keyboard: Ctrl+Up/Down move to the previous or next enabled, visible property (Ctrl+Home/End the first or last); a switch takes the plain arrows and Home/End as well. Tab is never trapped.

Example: Unit editor: a number with a unit picker; Width is above its maximum, so it shows its message naming the unit

```html
<pk-property-grid config='{"groups":[{"heading":"Size","fields":[{"key":"width","type":"unit","label":"Width","units":["px","%","rem"],"min":0,"max":100,"required":true},{"key":"gap","type":"unit","label":"Gap","units":["px","rem"]}]}]}' values='{"width":{"value":140,"unit":"%"},"gap":{"value":8,"unit":"px"}}'></pk-property-grid>
```

## `pk-radio-group`

**Radio group** (Form controls). One choice from a few options, as radios or as a segmented bar. The options are its own option children; the radios are native inputs, so arrow keys and grouping are the browser's.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `name` | string | `""` |  | The form field name. |
| `value` | string | `""` |  | The current value; what the form submits. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `label` | string | `""` |  | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `description` | string | `""` |  | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` |  | Blocks interaction; also set by a disabled fieldset. |
| `required` | boolean | `false` |  | The form cannot be submitted while the value is empty. |
| `invalid` | boolean | `false` |  | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |
| `variant` | enum | `"radio"` | `radio` `segmented` | radio draws a list of radios; segmented draws one bar of segments. |
| `direction` | enum | `"vertical"` | `vertical` `horizontal` | Radio variant: stacked or in a wrapping row. |
| `fill` | boolean | `false` |  | Segmented variant: spread the segments across the full width. |

**Slots**

| Slot | Description |
|---|---|
| (default) | option elements (value, disabled and text). |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-value-change` | `{ value: string }` | The user changed the value (same moment as change; carries it). |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Focuses the chosen radio, or the first enabled one. |

**CSS parts**

| Part | Description |
|---|---|
| `group` | The radiogroup container. |
| `option` | One option label (dynamic). |
| `option-label` | The option text (dynamic). |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-radio-border` | `var(--color-input-border)` | Border colour of a radio. |
| `--pk-segmented-active` | `var(--color-accent-fill)` | Background of the chosen segment. |

**Accessibility.** role=radiogroup named by label; each option is a native radio in one tab stop with arrow-key movement; the segmented variant hides the radio visually but keeps it focusable and shows the focus ring on the segment. Options are 44px tall on a phone.

Example: Radios

```html
<pk-radio-group label="Condition" value="nm"><option value="nm">Near mint</option><option value="vf">Very fine</option><option value="g" disabled>Graded (unavailable)</option></pk-radio-group>
```

## `pk-range`

**Range** (Form controls). A slider, or with dual a minimum and maximum slider, drawn from tokens with a live value output.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | The form field name. |
| `min` | number | `0` | Lowest value. |
| `max` | number | `100` | Highest value. |
| `step` | number | `1` | Increment. |
| `value` | number | `0` | Single mode: the value. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `value-low` (property `valueLow`) | number | `0` | Dual mode: the low value. |
| `value-high` (property `valueHigh`) | number | `100` | Dual mode: the high value. |
| `dual` | boolean | `false` | Two thumbs: a low and a high value; the low thumb never passes the high one. |
| `label` | string | `""` | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `description` | string | `""` | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` | Blocks interaction; also set by a disabled fieldset. |
| `output` | boolean | `false` | Shows the current value under the slider. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-range` | `{ value: number, low: number, high: number }` | The user committed a value (single) or a pair (dual). |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Focuses the first thumb. |

**CSS parts**

| Part | Description |
|---|---|
| `wrap` | The slider area. |
| `track` | The track with its fill. |
| `control` | The single slider, or the low thumb. |
| `control-high` | The high thumb (dual). |
| `output` | The value text. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-range-thumb` | `1.25rem` | Thumb size. |
| `--pk-range-fill` | `var(--color-accent)` | Colour of the filled track and the thumb. |
| `--pk-range-track` | `a mix of muted and the input colour` | Colour of the empty track. |

**Accessibility.** Native range inputs: arrow keys, Home, End and Page keys; each thumb of a dual slider is named "<label> minimum" and "<label> maximum". The 44px phone hit area comes from the taller wrapper. A form receives one entry (single) or two entries under the same name (dual).

Example: Single and dual

```html
<pk-range label="Volume" value="40" output></pk-range>
<pk-range label="Price range" dual value-low="20" value-high="80" output></pk-range>
```

## `pk-rating`

**Rating** (Form controls). A whole-star rating input (radios underneath), and a compact read-only display form.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | The form field name. |
| `value` | number | `0` | The rating, 0 for none. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `max` | number | `5` | Number of stars. |
| `readonly` | boolean | `false` | The display form: not interactive, exposed as an image with a text alternative. |
| `label` | string | `""` | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `disabled` | boolean | `false` | Blocks interaction; also set by a disabled fieldset. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-rating-change` | `{ value: number }` | The user changed the value (same moment as change; carries it). |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Focuses the chosen star. |

**CSS parts**

| Part | Description |
|---|---|
| `group` | The stars container. |
| `star` | One star (dynamic). |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-rating-color` | `var(--field-warn)` | Colour of a lit star. |

**Accessibility.** An input rating is a radio group: each star is named "3 stars", arrow keys change the value and focus is visible. The read-only form is role=img with "4 out of 5 stars". Stars are 44px on a phone.

Example: Input and display

```html
<pk-rating label="Your rating" value="3"></pk-rating>
<pk-rating readonly value="4"></pk-rating>
```

## `pk-select`

**Select** (Form controls). The browser select in the same skin as the text input. Its options are its own option and optgroup children; a phone shows its native picker.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | The form field name. |
| `value` | string | `""` | The current value; what the form submits. A form reset or browser restore sets it silently, as a native control does (pk-form raises pk-reset after a reset). |
| `label` | string | `""` | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `show-label` (property `showLabel`) | boolean | `false` | Shows label as visible text above the field (linked to it), for use without a pk-field. |
| `description` | string | `""` | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` | Blocks interaction; also set by a disabled fieldset. |
| `required` | boolean | `false` | The form cannot be submitted while the value is empty. |
| `invalid` | boolean | `false` | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |
| `warning` | boolean | `false` | Shows the warning state (a value the user may want to check). |
| `valid` | boolean | `false` | Shows the valid state. |
| `width` | string | `""` | Width: xs to 2xl, or a CSS length. |
| `keep-unknown-value` (property `keepUnknownValue`) | boolean | `false` | When the value is not offered by any option (a saved id whose list has not loaded, an option since retired), keep it: a stand-in option holding the value is added first and selected, so the control never shows a value it does not hold and a save does not write the first option back. Single select only. |
| `unknown-value-label` (property `unknownValueLabel`) | string | `""` | The text of the stand-in option added by keepUnknownValue. Empty shows the value itself. The option carries data-pk-unknown. |
| `multiple` | boolean | `false` | Allows several selections (value is comma-separated with a comma inside an option value written as a backslash and a comma, a backslash before a comma or backslash doubled; the form submits one entry each). |
| `values` | json | `[]` | The selection as an array of strings (one entry for a single select, none when empty); the typed form of value, which stays the comma-separated string for forms. Set multiple first. The commit event carries it as detail.values. |
| `options` | json | `[]` | Options as data: a flat array of { value, label, disabled? }, a plain array of strings, or grouped as [{ group, options: [...] }]. Rendered into the inner select. When set (non-empty), it wins over slotted option/optgroup children and a warning is logged once. |

**Slots**

| Slot | Description |
|---|---|
| (default) | option and optgroup elements; ignored while options is set. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-value-change` | `{ value: string, values: string[] }` | The user committed a new value (same moment as change; carries the value). |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Moves focus to the select. |

**CSS parts**

| Part | Description |
|---|---|
| `label` | The visible label shown by showLabel. |
| `box` | The bordered field. |
| `control` | The inner select. |
| `chevron` | The drop-down arrow. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-bg` | `var(--color-input)` | Background of the field. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |

**Accessibility.** A native select in the shadow root: keyboard, type-to-select and the platform picker are the browser's own. Named by label, described by description.

Example: Select and multiple

```html
<pk-select label="Status" value="Draft"><option>Active</option><option>Draft</option><option>Archived</option></pk-select>
<pk-select label="Formats" multiple value="JSON"><option>CSV</option><option>JSON</option><option>XML</option></pk-select>
```

## `pk-select-menu`

**Select menu** (Forms & inputs). A single-choice listbox popup that reads native option children, so it degrades to nothing worse than a list; form-associated, keyboard driven with typeahead.

It is form-associated (takes part in a `<form>` by its `name`).

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `value` | string | `""` | Selected option value. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `name` | string | `""` | Form field name. |
| `placeholder` | string | `"Select…"` | Shown with no selection. |
| `open` | boolean | `false` | Whether the list is shown. |
| `disabled` | boolean | `false` | Prevents interaction |
| `required` | boolean | `false` | A value is required |

**Slots**

| Slot | Description |
|---|---|
| (default) | option elements. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-change` | `{ value: string, label: string, previous: string }` | The selection changed. |
| `pk-open` | `none` | The list opened. |
| `pk-close` | `{ reason: string }` | A close was asked for (Escape, outside press, choosing, the trigger). Cancelable: preventDefault keeps it open. |

**Methods**

| Method | Description |
|---|---|
| `show()` | Opens it |
| `hide()` | Closes it |

**CSS parts**

| Part | Description |
|---|---|
| `trigger` | The button that opens the list |
| `value` | The selected value text |
| `caret` | The open/close arrow |
| `list` | The option list (rows are built inside it) |

**Accessibility.** The trigger is a combobox with aria-haspopup=listbox and aria-expanded; the list is role=listbox with option children and aria-activedescendant; arrows, Home, End, typeahead, Enter, Escape.

Example: Select menu

```html
<pk-select-menu name="status" value="draft"><option value="draft">Draft</option><option value="active">Active</option><option value="archived">Archived</option></pk-select-menu>
```

## `pk-switch`

**Switch** (Form controls). An on/off toggle that takes part in a form: it submits its value when on and resets with the form.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `checked` | boolean | `false` |  | Whether the switch is on. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `disabled` | boolean | `false` |  | Blocks interaction; also set by a disabled fieldset. |
| `name` | string | `""` |  | The form field name. |
| `value` | string | `"on"` |  | The value submitted while checked. |
| `invalid` | boolean | `false` |  | Shows the invalid state (a red ring) and sets aria-invalid. |
| `size` | enum | `"md"` | `sm` `md` `lg` | Track size. |
| `label-position` (property `labelPosition`) | enum | `"end"` | `end` `start` `between` | end puts the label after the switch, start before it, between pushes them to opposite edges of the row. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The label. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `change` | `{ checked }` | The user toggled the switch (a native-style event). |
| `pk-change` | `{ checked: boolean }` | The same toggle as a typed custom event; the Blazor wrapper listens to this one. |

**CSS parts**

| Part | Description |
|---|---|
| `control` | The inner button. |
| `track` | The track. |
| `thumb` | The thumb. |
| `label` | The label wrapper. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-switch-on` | `var(--color-accent)` | Track colour when on. |
| `--pk-switch-off` | `var(--color-border)` | Track colour when off. |

**Accessibility.** An inner button with role=switch and aria-checked, named by the label slot. Space and Enter toggle it. In a form it contributes name=value while checked, resets to its initial state with the form, and is disabled by a disabled fieldset. Use it only for settings that take effect at once; a value that needs Save is a checkbox.

Example: In a form

```html
<form>
  <pk-switch name="notify" checked>Email me</pk-switch>
  <pk-button type="submit">Save</pk-button>
</form>
```

## `pk-tag-input`

**Tag input** (Form controls). Free-text tags typed into one control: Enter or a separator adds, paste splits, Backspace in an empty field removes the last, duplicates and a maximum are handled.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | The form field name. |
| `value` | string | `""` | The tags, comma-joined (a comma inside a tag is written as a backslash and a comma; read the tags property for the list). A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `values` | json | `[]` | The tags as an array of strings (any text, commas and backslashes included); the typed form of value, which stays the comma-joined string for forms. Setting it (or its JSON attribute) sets value; reading it parses value. The commit event carries it as detail.values. |
| `placeholder` | string | `""` | Hint shown in the empty field. |
| `label` | string | `""` | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `description` | string | `""` | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` | Blocks interaction; also set by a disabled fieldset. |
| `required` | boolean | `false` | The form cannot be submitted while the value is empty. |
| `invalid` | boolean | `false` | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |
| `separators` | string | `","` | Characters that end a tag (besides Enter). |
| `max` | number | `0` | Most tags allowed; 0 means no limit. |
| `allow-duplicates` (property `allowDuplicates`) | boolean | `false` | Allows the same tag twice (case-insensitive by default). |
| `locked-tags` (property `lockedTags`) | string | `""` | Always-present tags, comma-joined, shown before the editable ones. Rendered without a remove button, excluded from value, from the form value and from Backspace removal; a locked tag with the same text as an editable one is still shown twice. Set lockedTitle for why they can't be removed. |
| `locked-title` (property `lockedTitle`) | string | `"Locked"` | Tooltip (title attribute) on every locked tag. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-tags-change` | `{ value: string, tags: string[], values: string[] }` | A tag was added or removed. |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Focuses the text field. |

**CSS parts**

| Part | Description |
|---|---|
| `box` | The bordered field holding tags and the text field. |
| `field` | The text input. |
| `status` | The visually hidden live region. |
| `tag` | One tag pill (dynamic); a locked tag (from lockedTags) carries data-locked and has no tag-remove. |
| `tag-label` | A tag's text (dynamic). |
| `tag-remove` | A tag's remove button (dynamic). |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-bg` | `var(--color-input)` | Background of the field. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |
| `--pk-tag-bg` | `var(--color-accent-fill)` | Tag pill background. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| each locked tag's own pill (from lockedTags, a dynamic shadow child) | `data-locked` | Marks a locked pill for styling (::part(tag)[data-locked]) and so it is skipped by the remove-button query. Not a host attribute; rebuilt every render. |

**Accessibility.** Each remove button has an aria-label ("Remove dc") and is out of the tab order (Backspace is the keyboard route); adds and removes are announced through a polite status element; a form receives one entry per tag. A locked tag has a title (lockedTitle) and no remove button, so it is not announced as removable.

Example: Tags

```html
<pk-tag-input label="Tags" value="dc,variant" max="8" placeholder="add a tag"></pk-tag-input>
```

## `pk-textarea`

**Textarea** (Form controls). A multi-line control that can grow with its content up to a cap.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `name` | string | `""` |  | The form field name. |
| `value` | string | `""` |  | The current value; what the form submits. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `placeholder` | string | `""` |  | Hint shown while empty. |
| `label` | string | `""` |  | The accessible name (aria-label). pk-field fills it from its own label when this is empty. |
| `show-label` (property `showLabel`) | boolean | `false` |  | Shows label as visible text above the field (linked to it), for use without a pk-field. |
| `description` | string | `""` |  | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` |  | Blocks interaction; also set by a disabled fieldset. |
| `readonly` | boolean | `false` |  | The value can be read but not edited. |
| `required` | boolean | `false` |  | The form cannot be submitted while the value is empty. |
| `invalid` | boolean | `false` |  | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |
| `warning` | boolean | `false` |  | Shows the warning state (a value the user may want to check). |
| `valid` | boolean | `false` |  | Shows the valid state. |
| `font` | enum | `"inherit"` | `inherit` `sans` `mono` | Font family of the text: sans or mono (for code, queries, ids). Default inherit. |
| `rows` | number | `3` |  | Visible rows of a fixed-height textarea. |
| `minlength` | string | `""` |  | Minimum text length. |
| `maxlength` | string | `""` |  | Maximum text length. |
| `autogrow` | boolean | `false` |  | Grows with the content instead of scrolling. |
| `max-height` (property `maxHeight`) | number | `0` |  | Auto-grow cap in pixels; 0 means no cap and the box scrolls beyond it otherwise. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The value changed (native, composed: bubbles out of the shadow root). Read event.target.value. |
| `change` | `native Event` | The user committed the value (re-dispatched from the inner control, composed). |
| `pk-value-change` | `{ value: string }` | The user committed a new value (same moment as change; carries the value). |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Moves focus to the textarea. |
| `select()` | Selects the text. |

**CSS parts**

| Part | Description |
|---|---|
| `label` | The visible label shown by showLabel. |
| `box` | The bordered field. |
| `control` | The inner textarea. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-bg` | `var(--color-input)` | Background of the field. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |
| `--pk-textarea-max-height` | `none` | Cap on the auto-grow height (set from max-height). |

**Accessibility.** A native textarea in the shadow root, named by label and described by description; invalid sets aria-invalid. Auto-grow never traps scrolling: a capped box scrolls internally.

Example: Visible label

```html
<pk-textarea label="Notes" show-label rows="3"></pk-textarea>
```

## `pk-unit-input`

**Unit input** (Form controls). A number and a unit select in one field for lengths such as 1.5rem or 24px; the value is the two joined, and a value with a unit outside the list keeps its unit.

It is form-associated (takes part in a `<form>` by its `name`) and delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | The form field name. |
| `value` | string | `""` | The number and unit joined, for example 1.5rem or 12px (empty until there is a number); what the form submits. A value that is not a number with a unit (auto, calc(...)) leaves the number field empty and is kept until the user edits. A form reset sets it back to its initial value without raising the commit event, as a native control does: pk-form raises pk-reset after it, so a host that mirrors the value reads it again there. A browser state restore also sets it silently. |
| `units` | string | `"px,rem,em,%"` | The units the select offers, comma or space separated; the first is used until a value or the user picks another. A value whose unit is not listed adds that unit to the select. |
| `placeholder` | string | `""` | Hint shown in the number field while it is empty. |
| `label` | string | `""` | The accessible name (aria-label) of the number field; the unit select is named "<label> unit". pk-field fills it from its own label when this is empty. |
| `show-label` (property `showLabel`) | boolean | `false` | Shows label as visible text above the field (linked to it), for use without a pk-field. |
| `description` | string | `""` | Help and error text, exposed as aria-description; pk-field fills it. |
| `disabled` | boolean | `false` | Blocks interaction; also set by a disabled fieldset. |
| `readonly` | boolean | `false` | Shows the value but does not let it change (the unit select is disabled too). |
| `required` | boolean | `false` | The form cannot be submitted while the number is empty. |
| `invalid` | boolean | `false` | Shows the invalid state and sets aria-invalid; set by pk-field or pk-form (or by a Blazor EditContext). |
| `min` | string | `""` | Lowest number (the number, whatever the unit). |
| `max` | string | `""` | Highest number. |
| `step` | string | `"any"` | The number's step: any (the default) allows decimals, or a size such as 0.25 that the arrow keys move by. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The number was edited (native, composed: bubbles out of the shadow root). Read the value from the element. |
| `change` | `native Event` | The user committed the number or picked another unit (re-dispatched from the inner control, composed). |
| `pk-value-change` | `{ value: string }` | The user committed a new value (same moment as change; carries the value). |

**Methods**

| Method | Description |
|---|---|
| `focus()` | Moves focus to the number field. |

**CSS parts**

| Part | Description |
|---|---|
| `label` | The visible label shown by showLabel. |
| `box` | The bordered field that holds the number and the unit. |
| `control` | The inner number input. |
| `unit` | The inner unit select. |
| `chevron` | The arrow drawn on the unit select. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-control-bg` | `var(--color-input)` | Background of the field. |
| `--pk-control-border` | `var(--color-input-border)` | Border colour. |
| `--pk-control-radius` | `var(--radius-sm)` | Corner radius. |

**Accessibility.** A native number input and a native select live in the shadow root: the number is named by label (aria-label) and described by description, the select is named "<label> unit", so both are reachable by Tab and the arrow keys change the number by step. invalid sets aria-invalid and the state is never colour alone (the ring is thicker). Text is 16px and the control 44px tall on a phone.

Example: Lengths

```html
<pk-unit-input label="Width" value="1.5rem"></pk-unit-input>
<pk-unit-input label="Gap" show-label units="px rem" value="12px" min="0"></pk-unit-input>
```
