Askr documentation
UI & Components

Form, Label, Field, and Input Group

Form, Label, Field, and Input Group: anatomy, keyboard behavior, state, and theming in Askr.

Example

import { Button, Field, FieldHint, FieldLabel, Input } from '@askrjs/themes/components';

<Field>
  <FieldLabel htmlFor="project-name">Project name</FieldLabel>
  <Input id="project-name" name="name" required />
  <FieldHint>Shown to everyone in the workspace.</FieldHint>
  <Button type="submit">Save project</Button>
</Field>

Published props

Generated from the TypeScript declarations shipped by the installed package. Named types in the Type column define the accepted values.

FormAsChildProps

Import from @askrjs/ui/form.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSX.Element;
refref?: Ref<Element>;

FormProps

Import from @askrjs/ui/form.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLFormElement>;

LabelAsChildProps

Import from @askrjs/ui/label.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

LabelLabelProps

Import from @askrjs/ui/label.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
htmlForhtmlFor?: string | undefined;
refref?: Ref<HTMLLabelElement>;

LabelOwnProps

Import from @askrjs/ui/label.

PropTypeDefaultDescription
childrenchildren?: unknown;

LabelProps

Import from @askrjs/ui/label.

PropTypeDefaultDescription
asChildasChild?: boolean | undefined;
childrenchildren?: unknown;
refref?: ((value: Element | null) => void) | { current: Element | null; } | ((value: HTMLLabelElement | null) => void) | { current: HTMLLabelElement | null; } | null | undefined;

Purpose

This page groups four small, composable pieces that don't stand well alone: `Form` (a semantic `<form>` wrapper from `@askrjs/ui/form`), `Label` (an accessible label with no visual opinion, from `@askrjs/ui/label`), and the themes-only `Field` and `InputGroup` layout primitives that give a labeled input row consistent spacing and error/hint slots. None of these own form validation logic — they're structural and accessibility building blocks you compose around whatever validation approach you're already using.

Install and import

`Form` and `Label` come from `@askrjs/ui/form` and `@askrjs/ui/label`; `Field` and `InputGroup` are themes-only and come from `@askrjs/themes/field` and `@askrjs/themes/input-group` since there's no headless behavior to separate out — they're pure layout components. All four are also re-exported from `@askrjs/themes/components` if you're already importing your styled catalog from there.

Live examples

The examples build up a typical labeled field: a `Field` wrapping a `Label`, an `InputGroup` containing an `Input`, and optionally a `FieldHint` or `FieldError` beneath it, all inside a `Form`. Each piece is shown both alone and composed, so you can see what each one actually contributes to the final markup.

Anatomy

`Form` is just a `<form>` (or, via `asChild`, whatever element you provide) with a `data-slot="form"` attribute — it doesn't intercept submission or manage field state. `Label` renders a native `<label>` by default. `Field` is a `<div>` that accepts an `invalid` flag and optionally contains `FieldHint`/`FieldError` paragraph children. `InputGroup` is a `<div>` that visually attaches its children (e.g. an input and a leading icon) via its `attached` prop.

State model

None of these four hold state themselves — `Form` doesn't track field values, and `Field`'s `invalid` prop is something you pass in based on validation you're already running elsewhere, not something the component derives. This is intentional: form state and validation belong to whatever data layer or form library you're using, and these pieces only handle structure and semantics around it.

Keyboard and accessibility

`Label`'s accessibility contract prioritizes `aria-label` over `textContent` for computing the accessible name, and its `for`/`htmlFor` association follows native label semantics — clicking the label focuses the associated control the same way a native `<label for>` does. Neither `Field` nor `InputGroup` changes tab order or keyboard behavior; they're layout-only, so keyboard behavior is entirely inherited from whatever control you place inside them.

Styling and tokens

`Field`'s `invalid` boolean prop sets a real `data-invalid="true"` attribute on the root — but the default theme's `field.css` has no rule keyed to `[data-invalid]`, so setting it doesn't change `Field`'s own appearance out of the box. It's there as a stable styling hook for your own CSS or a custom theme to key off, not a treatment `@askrjs/themes` ships pre-wired; any visible invalid state you see in practice comes from styling the control inside the field (e.g. Input's own `aria-invalid` border swap), not from `Field` itself. `InputGroup` takes an `orientation` (`horizontal`/`vertical`) and an `attached` boolean that removes the gap and shared borders between grouped children, which is the look you want for a search box with a leading icon or a trailing button.

API

`Form` takes only `children` plus standard `BoxProps<'form'>`. `Label` takes `children` and, in its non-`asChild` form, `htmlFor` plus the rest of native label props. `Field` takes `invalid` and native `div` props; `FieldHint` and `FieldError` take native paragraph props — the error text itself is whatever you render as children, but `FieldError` isn't purely presentational the way `FieldHint` is: it renders with `role="alert"` baked in, so its content gets announced to assistive tech as soon as it mounts. `InputGroup` takes `attached` and `orientation`; `InputGroupText` renders inline decorative or label text inside the group and supports `asChild` like most other parts in the library.

Edge cases

Because `Field`, `FieldHint`, `FieldError`, and `InputGroup` don't manage any state, there's no automatic wiring between `invalid` on `Field` and the appearance of `FieldError` — if you want the error message to only show when the field is invalid, that's a condition you write yourself in JSX. Note also that the broader `Field*` family you'll see in the styled catalog (`FieldContent`, `FieldGroup`, `FieldLegend`, `FieldSet`, `FieldTitle`, and so on) lives in `@askrjs/themes/components` as additional composition helpers beyond the core `Field`/`FieldHint`/`FieldError` trio documented here.

For the individual controls you'll typically place inside a `Field`, see Input, Textarea, Checkbox, Select, and Switch. For native browser form fallbacks and OTP-style multi-box input, see Native Select and Input OTP under experimental controls.