# Elements: Form layout

> 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-layout.md` (kept apart so the group files stay short).

## `pk-field`

**Field** (Form layout). The label, help, error, warning and counter around a form control, with the wiring between them. It hands its label and messages to the control, because ids do not cross shadow roots.

It is delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `label` | string | `""` |  | The label text (or use the label slot). |
| `help` | string | `""` |  | Help text under the control. |
| `error` | string | `""` |  | An error message; puts the control into the invalid state. |
| `warning` | string | `""` |  | A warning message; puts the control into the warning state. |
| `required` | boolean | `false` |  | Marks the field required (a star, and required on the control). |
| `max` | number | `0` |  | Counter maximum: shows length / max and turns warm from 90% and red past the max; 0 hides the counter. |
| `layout` | enum | `"stack"` | `stack` `row` `inline` | stack puts the label above the control; row puts it in a fixed column beside it (stacks on a phone); inline puts label and control on one line at their natural width. |
| `action-end` (property `actionEnd`) | boolean | `false` |  | Pins the label-action slot to the end of the label row (a figure or a small action opposite the label text) instead of directly after it. |
| `size` | enum | `"md"` | `md` `sm` | sm is the compact field of a toolbar or filter row: smaller label, tighter gap. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The control: an pk-* form control or a native input, select or textarea. |
| `label` | A rich label (overrides the label prop). |
| `label-action` | A small action beside the label, such as a reset to source button (a pk-button size mini, variant ghost). Clicking it does not focus the control. |
| `help` | Rich help text (overrides the help prop). |

**CSS parts**

| Part | Description |
|---|---|
| `field` | The grid that lays out label, control and foot. |
| `label` | The label wrapper (click focuses the control). |
| `required` | The required star. |
| `foot` | The row under the control. |
| `help` | The help text. |
| `error` | The error message (a polite live region). |
| `warning` | The warning message (a polite live region). |
| `count` | The character counter. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-field-label` | `var(--color-text)` | Label colour. |
| `--pk-field-label-width` | `9rem` | Label column width of the row layout. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the slotted control (a pk-* element or a native input, select or textarea) | `label`, `description`, `required`, `invalid`, `warning`, `aria-label`, `aria-description`, `aria-invalid`, `data-invalid` | A pk-* control takes the label, description, required, invalid and warning props (each only when the control has that prop, and label only when the control has none of its own); a native control takes aria-label (when it has none), aria-description, aria-invalid and data-invalid. aria-description and aria-invalid are removed again when the help or error goes. The props and attributes stay when the field disconnects. |

**Accessibility.** The label click focuses the control. Ids cannot cross shadow roots, so the field sets label, description (help plus error), invalid, warning and required on an pk-* control, or aria-label and aria-description on a native one. Messages are polite live regions that are empty until used; the star is aria-hidden because required is set on the control.

Example: Help, required and counter

```html
<pk-field label="Product title" required help="Shown on the storefront and invoices." max="80"><pk-input value="Widget 1 Cover A"></pk-input></pk-field>
```

## `pk-field-group`

**Field group** (Form layout). A form drawn from a list of field specs: each entry becomes a pk-field with the right control (pk-input of every scalar type, pk-textarea, pk-select, pk-checkbox, pk-switch, pk-range, pk-combobox). Set fields (label, kind, hint, required, constraints, options, when) and values ({ key: value }); a committed change updates values and raises pk-field-change. A field with when (data: { field, equals | in | not }) or a visible(spec, values) callback property is not rendered at all while it is hidden, so a hidden required field can never block a submit; its value stays in values. A spec can also carry declarative conditions over the live committed values as text: whenExpr (or when given as a string), disabledExpr, readonlyExpr and requiredExpr, plus labelWhenExpr and helpWhenExpr for a label or help that follows the values, for example "status == 'paid' && amount > 100" (see the fields prop for the grammar); they are evaluated in the browser, by a module that loads only when a spec declares one. A combobox field takes its options from the search({ key, query }) callback property when it is set (a promise of [{ value, label }], like pk-combobox's pk-combo-query): the combobox then does not filter them itself. With a commit({ key, value, values }) callback property set, a committed change is handed to the host first and pk-field-change is raised only when it accepts: the field shows a busy state (aria-busy on the control and a small spinner beside the label) while the promise is pending, the last edit of a field wins (a stale answer changes nothing), and a rejection or a returned string shows as that field's error message with the typed text kept. The controls live in this element's shadow tree. The element is form-associated: a surrounding form gets one FormData entry per shown field (a checkbox or switch only when checked; a hidden or disabled field none) and the first invalid field's message as its validity, anchored on that field, which is focused when a submit or reportValidity() fails; pk-form lists every problem. A form reset puts the values back to the ones last assigned.

It is form-associated (takes part in a `<form>` by its `name`).

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `fields` | json | `[]` |  | The field specs, in order: { key, label, kind?, hint?, help?, required?, whenExpr?, disabledExpr?, readonlyExpr?, requiredExpr?, labelWhenExpr?, helpWhenExpr?, readonly?, disabled?, hideLabel?, span?, columnSpan?, labelActionEnd?, suffixAction?: { icon, label, key }, cols?, placeholder?, autocomplete?, min?, max?, step?, minlength?, maxlength?, pattern?, format?, decimals?, currency?, locale?, selectOnFocus?, rows?, options?: [{ value, label }], free?, clearable?, mode?, debounce?, styled?, when?, msg? }. kind is text (default), number, email, password, date, datetime-local, time, url, tel, textarea, select (native control; styled: true gives the themed list; placeholder adds an empty first option that shows while the value is unset), checkbox, switch, range or combobox (debounce: ms to wait after typing before the search runs; mode: "select" makes it a styled select that opens a list, the way to get a themed list instead of the browser's), or custom (no control: the field's body is the child with slot editor-<key>; it has no value and takes no part in the form or its validation). hint is the line under the field, help a tooltip beside the label, span makes the field fill the row, columnSpan (2 or 3) makes it take that many columns of a multi-column group (one column on a narrow group; a span that reaches every column fills the row), labelActionEnd pins the content of the label-action-<key> slot (any kind, custom included) to the end of the label row, suffixAction draws an icon button inside the end of an input kind's control (icon: an icon name, label: its accessible name, key: what pk-field-action reports; follows the field's disabled and readonly state), cols (1 to 12) is the width in twelfths of the row: one field with it puts the whole group on a 12-column grid where consecutive fields share a row and wrap when they do not fit, a field without it fills the row, and a narrow group (under 34rem) gives none less than half the row, datetime-local is a date and time of day with no time zone (value, min and max are yyyy-MM-ddTHH:mm text), format is money on an input kind (with decimals, currency, locale: pk-input's money format, the value stays the plain number), selectOnFocus selects the whole value when an input kind gains focus (the money format included), msg is { constraint: text } for the form's validation messages. Conditions: whenExpr (visible while true; `when` as a string is the same), disabledExpr, readonlyExpr and requiredExpr are expressions over the other fields' committed values: keys, text and number and true/false literals, == != < <= > >=, `key in [a, b]`, ! && \|\| and parentheses; a bare key is true when not empty, false or 0. No function calls or member access; nothing is evaluated as code. labelWhenExpr and helpWhenExpr are conditional text for the label and the help tooltip: `cond ? 'text' : cond ? 'text' : other`, where a value is a 'text' or another field's key (that field's value), for example "status == 'paid' ? 'Paid on' : due"; the first true condition wins, and an empty answer keeps label or help. An unknown key or a syntax error is warned once and reads as true for whenExpr, false for the other conditions, and keeps the plain label or help. |
| `values` | json | `{}` |  | The current values, { key: value }: a string, a boolean (checkbox, switch) or a number (range). The element keeps its own copy and replaces this property when the user commits a change (it never edits the object you gave it); pk-field-change carries the same values. |
| `name` | string | `""` |  | The form field name of the group. See prefix. |
| `prefix` | boolean | `false` |  | Submit each field as name.key instead of key (a form with two groups that use the same keys). |
| `label` | string | `""` |  | The group's accessible name. |
| `columns` | number | `1` |  | How many columns the fields flow into, up to as many as fit (a column is at least 16rem wide, so a narrow group and a phone show one); a field with span fills the row and one with columnSpan takes that many columns. |
| `layout` | enum | `"grid"` | `grid` `inline` | grid (the form-section grid: columns, span and cols apply) or inline: the fields sit in one wrapping row, bottom-aligned with the content of the end slot, for a short add-row. A field is 12rem wide (shrinking) unless it has cols (1 to 12), which sizes it as twelfths of the row exactly as in the grid (never under 5rem), so a short quantity stays short beside a wide picker. span, columnSpan and columns are ignored inline. Bottom alignment is of the whole field, so a hint under one field lifts its control: keep hints out of an add-row. |
| `disabled` | boolean | `false` |  | Disables every control; also set by a disabled fieldset. |
| `readonly` | boolean | `false` |  | Makes every control read-only. |

**Slots**

| Slot | Description |
|---|---|
| `end` | Trailing content on the fields' row with layout="inline" (the Add and Cancel buttons of an add-row), bottom-aligned with the controls. |
| (default) | Holds children whose slot is label-action- followed by a field key: each appears beside that field's label (a button, a badge). A child with no such slot name is not shown. |
| `editor-<key>` | The body of the custom field with that key: your own content in place of a control. It has no value and takes no part in the form or its validation. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-field-change` | `{ key: string, value: object, values: object }` | The user committed a change to one field: its key and value, and a fresh copy of every value. Fields whose when depends on it have already been shown or hidden. |
| `pk-field-action` | `{ key: string, action: string }` | The user pressed a field's suffixAction button: the field's key and the action's key. |

**Methods**

| Method | Description |
|---|---|
| `problems()` | Every invalid shown field, in order: [{ key, label, message, control, focus() }]. pk-form lists these in its summary and focuses the first. |
| `report(on = true)` | Shows each invalid field's message in its pk-field (on), or clears every message (off). |
| `checkField(keyOrNode)` | Re-checks one field, showing or clearing its message; returns whether it is valid. pk-form uses it for live checks. |
| `snapshot()` | The values as the controls hold them now, { key: value }: what the user has typed even before it committed, with the committed value for a field that is not shown. |
| `setError(key, message)` | Shows a message in one field, for an answer from the server; an empty message clears it, and report(false) clears every message. |
| `focusField(key)` | Focuses one field's control. |
| `focus()` | Focuses the first invalid field, else the first field. |
| `patch(fields)` | Sets fields without rebuilding the controls of the fields that stay (same key): their disabled, readonly, label and help change in place, so the control the user is typing in keeps its focus and text; fields that left are removed and new ones drawn. A page whose conditions follow the edits (pk-record-page) uses it. |
| `refresh()` | Draws the fields again; call it after changing the visible(spec, values) callback property. |

**CSS parts**

| Part | Description |
|---|---|
| `group` | The grid that holds the fields. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the pk-field elements it builds in its own shadow tree | `error`, `required` | requiredExpr, report(), checkField() and a form reset show or clear each field's message on the pk-field this element created and owns in its own shadow tree (never a host-given node); the fields are rebuilt from fields and values and removed with the element. |

**Accessibility.** The host is role=group with the label as its name. Each field is a pk-field: a visible label, the hint under it and the error in its own region; the label, description and invalid state reach the control as properties, so nothing depends on an id crossing a shadow root. A field whose label is hidden (hideLabel) keeps it as the control's aria-label. A hidden field is removed from the tree, not just visually hidden.

Example: A suffix action: an icon button inside the input, reported as pk-field-action

```html
<pk-field-group label="Item" fields='[{"key":"sku","label":"SKU","suffixAction":{"icon":"search","label":"Find SKU","key":"find"}}]'></pk-field-group>
```

## `pk-field-row`

**Field row** (Form layout). A wrapping row of fields that share the width: each field grows from a 120px minimum, a wide field takes a row of its own.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `align` | enum | `"stretch"` | `stretch` `end` `center` | Vertical alignment of the fields in the row. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The fields, checkboxes or switches. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-field-row-gap` | `var(--space-4)` | Gap between the fields. |
| `--pk-field-row-margin` | `var(--space-3)` | Space below the row. |

**Accessibility.** Plain layout with no role of its own.

Example: Two fields

```html
<pk-field-row><pk-field label="First"><pk-input></pk-input></pk-field><pk-field label="Second"><pk-input></pk-input></pk-field></pk-field-row>
```

## `pk-form`

**Form validation** (Form layout). Wraps a form and shows the browser's constraint validation well: messages in each field, a summary with links, focus on the first problem, and live re-checking. A pk-field-group inside the form is looked through: its fields show their own messages, and each invalid field is listed in the summary and can be focused from it.

It is delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `validate` | enum | `"blur"` | `blur` `input` `submit` | When to check: blur (after leaving a field, and on submit), input (while typing) or submit (only on submit). A field already showing an error re-checks as you type. |
| `show-valid` (property `showValid`) | boolean | `false` |  | Also shows the valid state on fields that hold a value. |
| `summary` | boolean | `false` |  | Shows a summary of the problems with links to the fields; each item starts with its field's name ("Code: Please fill out this field."). |

**Slots**

| Slot | Description |
|---|---|
| (default) | A form element. |
| `summary-title` | The summary heading. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-invalid` | `{ count: number, controls: Element[], messages: string[], problems: { key, label, message }[] }` | A submit was stopped because these controls are invalid. count, messages and problems have one entry per problem; a form-associated group of fields (pk-field-group) is listed once in controls and once per invalid field in problems. |
| `pk-valid` | `null` | A submit passed validation. |
| `pk-reset` | `none` | The form was reset and every control has its initial value again. A reset raises no input or change event on the controls (as with native controls), so a host that mirrors their values reads them again here. Not raised when the reset was cancelled. |

**Methods**

| Method | Description |
|---|---|
| `validateAll()` | Checks every control now; returns true when all are valid. |

**CSS parts**

| Part | Description |
|---|---|
| `summary` | The problem summary box. |
| `summary-title` | The summary heading. |
| `summary-list` | The list of problems. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-form-summary-bg` | `var(--color-surface-alt)` | Summary background. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| controls of the form (a pk-field wrapper, or the control itself) | `error`, `invalid`, `valid`, `aria-invalid` | On validation the message goes to the enclosing pk-field as its error prop; a control without one gets its invalid prop, or aria-invalid on a native control (removed again when the message clears); valid is set on a control that has it when show-valid is on. Nothing is undone on disconnect. |

**Accessibility.** Each summary item starts with the name of its field (the pk-field label, else the control's aria-label or label, else its name or id, else its tag and position), so it reads "Code: Please fill out this field." and a screen reader user hears which field has the problem; the link still moves focus to the control. Turns the native bubble off and puts each message in the field as a polite live region with aria-invalid on the control; the first invalid control takes focus on submit; the summary is role=alert and its links move focus. Custom text per constraint: data-msg-required, data-msg-type, data-msg-pattern, data-msg-min, data-msg-max on the control.

Example: Validated form with summary

```html
<pk-form summary>
  <form>
    <pk-stack>
    <pk-field label="Name" required><pk-input name="name" required data-msg-required="Enter a name."></pk-input></pk-field>
    <pk-field label="Email" required><pk-input name="email" type="email" required data-msg-type="Use the form name@example.com."></pk-input></pk-field>
    <pk-button type="submit">Save</pk-button>
    </pk-stack>
  </form>
</pk-form>
```

## `pk-form-actions`

**Form actions** (Form layout). The bar of buttons under a form, aligned start, end or between, and sticky to the bottom of a long form.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `sticky` | boolean | `false` |  | Sticks to the bottom of the scrolling area with a top edge, clearing the phone home indicator. |
| `align` | enum | `"start"` | `start` `end` `between` | Where the buttons sit. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The buttons. |
| `status` | A status message such as "Unsaved changes". |

**CSS parts**

| Part | Description |
|---|---|
| `bar` | The bar. |
| `status` | The status text. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-form-actions-bg` | `var(--color-panel)` | Background of a sticky bar. |

**Accessibility.** Plain layout; the status text is not a live region (use a Notice for announcements). On a phone the buttons of a sticky bar share the width.

Example: End-aligned

```html
<pk-form-actions align="end"><pk-button variant="ghost">Cancel</pk-button><pk-button>Save</pk-button></pk-form-actions>
```

## `pk-form-section`

**Form section** (Form layout). A titled section of a form: heading and description beside a responsive grid of fields (stacked at 1024px and below).

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `heading` | string | `""` | The section heading (or use the heading slot). |
| `description` | string | `""` | A sentence under the heading. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The fields. Give a child class form-span to make it span the full width. |
| `heading` | A rich heading. |
| `description` | A rich description. |

**CSS parts**

| Part | Description |
|---|---|
| `section` | The two-column grid. |
| `head` | The heading block. |
| `title` | The heading (h2). |
| `description` | The description. |
| `body` | The grid of fields. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-form-section-column` | `14rem` | Minimum column width of the field grid. |

**Accessibility.** The heading is a real h2 so the section is navigable by heading; the fields keep their own labels.

Example: Two sections

```html
<pk-form-section heading="Profile" description="Shown on invoices."><pk-field label="Business name"><pk-input></pk-input></pk-field><pk-field label="Website"><pk-input type="url"></pk-input></pk-field></pk-form-section>
```

## `pk-record-form`

**Record form** (Form layout). The page layout of a create-or-edit record around your own form: a toolbar (Cancel, your actions, Delete, Save), optional tabs, an error alert, the form in a pk-form (a summary of the problems, focus on the first, live checks), and an optional sidebar of status cards beside it (pk-detail-layout, one column on a phone). It owns no values, no load and no save: put your own <form> (and its fields) in the default slot, listen for pk-record-save when it validates, and keep the state yourself. Save submits your form, so pk-form checks it first. With actions-in-header the toolbar row is left out and a Save button in the page header calls submit() instead. On a phone the toolbar buttons fold to their icons (the words stay their names).

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `save-label` (property `saveLabel`) | string | `"Save"` |  | The Save button's text. Change it only for a page whose submit is an action ("Record payout"). |
| `delete-label` (property `deleteLabel`) | string | `"Delete"` |  | The Delete button's text. |
| `busy-text` (property `busyText`) | string | `"Saving…"` |  | The Save button's text while busy. |
| `save-disabled` (property `saveDisabled`) | boolean | `false` |  | Disables Save (a locked record, or nothing to save yet). Independent of busy. |
| `busy` | boolean | `false` |  | Save shows its busy state and Delete is disabled while the page saves or deletes. |
| `error` | string | `""` |  | A message shown in an error alert above the form; empty shows none. |
| `cancellable` | boolean | `false` |  | Shows a Cancel button first in the toolbar (pk-record-cancel). |
| `deletable` | boolean | `false` |  | Shows a Delete button (warn variant, disabled while busy) before Save (pk-record-delete). |
| `tabs-from` (property `tabsFrom`) | enum | `"phone"` | `phone` `tablet` `always` | Where the tabs in the tabs slot take effect. An element in the form or the sidebar with a tab attribute (a tab value, or several separated by spaces) belongs to those tabs: at or below the width named (phone, tablet), or always, only the chosen tab's sections show and the strip is visible; above it every section shows (the two-column layout) and the strip is hidden. Sections with no tab attribute always show. The element sets hidden on the tagged sections. |
| `tab-param` (property `tabParam`) | string | `""` |  | A query parameter name ("tab") that keeps the chosen tab in the address bar, so a reload or a shared link opens on it. Other parameters and the hash are kept and no history entry is added. Empty keeps nothing. A value that matches no enabled tab is ignored. |
| `tabs` | json | `[]` |  | The tabs as data: an array of { value, label, count?, visible? }. The element draws the strip (a pk-tabs in the tabs slot wins); count is the badge after the label, and a tab with visible false is left out (a chosen tab that went falls back to the first). Sections tag themselves with tab="value" as with tabs-from. Empty draws nothing. |
| `actions-in-header` (property `actionsInHeader`) | boolean | `false` |  | Leaves the toolbar row out so the buttons can sit in the page's own header and stay visible in its sticky title bar. Put your own Save button in the header and have it call submit() (a form attribute cannot cross this element's shadow root). |

**Slots**

| Slot | Description |
|---|---|
| (default) | Your own form element, with its fields. pk-form wraps it. |
| `sidebar` | Status and flag cards beside the form (pk-detail-layout). Left empty, the form takes the whole width. |
| `actions` | Extra toolbar buttons, between Cancel and Delete. |
| `tabs` | A pk-tabs strip for a record with more than one job, under the toolbar. Tag sections with a tab attribute (see tabs-from) and the chosen tab shows its sections, or render the chosen tab's content yourself. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-record-save` | `none` | Save was pressed (or submit() called, or the form submitted) and pk-form found the form valid: save the record. An invalid form raises no event; pk-form shows the problems. |
| `pk-record-cancel` | `none` | Cancel was pressed. |
| `pk-record-delete` | `none` | Delete was pressed. |

**Methods**

| Method | Description |
|---|---|
| `submit()` | Submits your form as its own submit button would: pk-form checks it, and a valid one raises pk-record-save. Call it from a button outside this element. |

**CSS parts**

| Part | Description |
|---|---|
| `shell` | The column: toolbar, tabs, error, then the layout. |
| `toolbar` | The row of buttons (left out with actions-in-header). |
| `cancel` | The Cancel button. |
| `delete` | The Delete button. |
| `save` | The Save button. |
| `strip` | The pk-tabs the element draws from the tabs array (the tabs slot's fallback). |
| `tabs` | The row holding the tabs slot; hidden when there are no tabs or tabs-from says they do not apply at this width. |
| `error` | The error alert. |
| `layout` | The pk-detail-layout holding the form and the sidebar. |
| `form` | The pk-form around your form. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| elements with a tab attribute in the form and the sidebar | `hidden` | A section tagged with a tab is hidden while another tab is chosen and tabs apply (tabs-from), and shown again when they stop applying. |

**Accessibility.** The buttons are real pk-buttons with their own names; Save is the form's submit, so Enter in a field submits as usual. pk-form's summary (role=alert) lists every problem with links that focus the field, and focuses the first invalid control. The error alert uses pk-alert's own role. Without a sidebar the page is one column; the sidebar follows the form in reading order.

Example: A record with a sidebar, Cancel and Delete (your own form in the default slot)

```html
<pk-record-form cancellable deletable>
  <form>
    <pk-stack>
      <pk-field label="Name" required><pk-input name="name" required></pk-input></pk-field>
      <pk-field label="Notes"><pk-textarea name="notes"></pk-textarea></pk-field>
    </pk-stack>
  </form>
  <pk-card slot="sidebar" heading="Status"><pk-badge variant="ok">Active</pk-badge></pk-card>
</pk-record-form>
```
