Tessera UI

Components

FormField

A label, a hint and an error around a control that has none of its own: a native input, or a control you built. It connects them for assistive technology, so the control is named, described and marked invalid.

The preview needs JavaScript. The code is in the Code tab.

FormField-0.tess
<template>
  <div>
    <FormField>
      <span slot="label">Start date</span>
      <input type="date" />
      <span slot="hint">The first day of the trial.</span>
    </FormField>
    <FormField required error="Pick a time.">
      <span slot="label">Reminder</span>
      <input type="time" />
    </FormField>
  </div>
</template>

Installation

npx @pixelscriptio/tessera add FormField
pnpm exec tessera add FormField
yarn tessera add FormField
bunx @pixelscriptio/tessera add FormField
  • Works with Tessera 0.9.0 or newer.
  1. Copy and paste the code into your project

    Put it in system/components.

    system/components/FormField.tess
    <contract>
    {
      "name": "FormField",
      "role": "generic",
      "description": "A label, a hint and an error around a control that has none of its own: a native input, or a control you built. It connects them for assistive technology, so the control is named, described and marked invalid.",
      "since": "0.8.0",
      "parts": ["root", "label", "control", "hint", "error"],
      "slots": {
        "label": "the visible label, required",
        "default": "the control: an input, a textarea, a select or an element with a role such as radiogroup",
        "hint": "help under the control"
      },
      "preview": {
        "slots": {
          "label": "Start date",
          "default": {
            "tag": "input",
            "attrs": {
              "type": "date"
            }
          },
          "hint": "The first day of the trial."
        }
      }
    }
    </contract>
    
    <script>
    const { error, required } = props({ error: '', required: false })
    
    // Ids of its own, so a page can have any number of fields.
    const uid = `t-field-${Math.random().toString(36).slice(2, 9)}`
    const box = state(null)
    const labelEl = state(null)
    
    // The control is whatever the slot holds. Name it with the label, describe it with the hint and the error, and mark it
    // invalid and required, as the component's own state says.
    effect(() => {
      const root = box.value
      const label = labelEl.value
      const message = error.value
      const must = required.value
      if (!root) return
      const control = root.querySelector('input, textarea, select, [role]')
      if (!control) return
      if (!control.id) control.id = `${uid}-control`
      if (label) label.setAttribute('for', control.id)
      control.setAttribute('aria-describedby', `${uid}-hint ${uid}-error`)
      if (message) control.setAttribute('aria-invalid', 'true')
      else control.removeAttribute('aria-invalid')
      if (must) control.setAttribute('aria-required', 'true')
      else control.removeAttribute('aria-required')
    })
    </script>
    
    <template>
      <div class="root">
        <label class="label" ref="labelEl">
          <slot name="label" />
          <span class="required" if="required" aria-hidden="true">*</span>
        </label>
        <div class="control" ref="box"><slot /></div>
        <span class="hint" :id="uid + '-hint'"><slot name="hint" /></span>
        <span class="error" :id="uid + '-error'" aria-live="polite">{error}</span>
      </div>
    </template>
    
    <style>
    .root {
      display: flex;
      flex-direction: column;
      gap: $space.1;
      color: $color.text.default;
      font-family: var(--t-font-family-sans);
      font-size: $font.size.sm;
    }
    
    .label {
      font-weight: $font.weight.bold;
    }
    
    .required {
      margin-left: $space.1;
      color: $color.action.danger.text;
    }
    
    .control {
      display: flex;
      flex-direction: column;
    }
    
    .hint {
      color: $color.text.muted;
    }
    
    .error {
      color: $color.action.danger.text;
    }
    
    .hint:empty,
    .error:empty {
      display: none;
    }
    </style>
    
  2. Check your design tokens

    Your design system needs these. tessera add reports the ones that are missing; by hand, run npx @pixelscriptio/tessera check.

    color.action.danger.text color.text.default color.text.muted font.size.sm font.weight.bold space.1

Usage

Using FormField

A label, a hint and an error around a control that has none of its own.

TextField, Textarea, Select, Combobox and RadioGroup already have a label, a hint and an error. Use FormField for the rest: a native <input type="date">, a range slider, or a control of your own.

  • The label is required, in the label slot. It is connected to the control, so clicking it focuses the control and a screen reader reads it.
  • The control goes in the default slot. FormField finds the first input, textarea, select or element with a role in it.
  • The hint is read with the control. The error is announced when it appears, is read with the control, and marks it invalid.
  • required shows a star and marks the control required for assistive technology. It does not stop anything being submitted: check the value yourself (see Form).

The preview needs JavaScript. The code is in the Code tab.

FormField-0.tess
<template>
  <div>
    <FormField>
      <span slot="label">Start date</span>
      <input type="date" />
      <span slot="hint">The first day of the trial.</span>
    </FormField>
    <FormField required error="Pick a time.">
      <span slot="label">Reminder</span>
      <input type="time" />
    </FormField>
  </div>
</template>

API reference

Slots

SlotContent
labelthe visible label, required
defaultthe control: an input, a textarea, a select or an element with a role such as radiogroup
hinthelp under the control

Anatomy

Rolegeneric
Partsroot label control hint error
Since0.8.0

Design tokens it uses

Your design system needs these. tessera add checks that it has them, and can add the ones it lacks.

color.action.danger.text color.text.default color.text.muted font.size.sm font.weight.bold space.1

Checksum

f8e7a1909d405237448d406e1bae662d4775e1b02e6f448d036fd5d86abcf195