Pages

Skip to content

Field ​

A component that shows the label, description, and errors of a form field, and a summary of errors after a failed submit.

UiField ​

A wrapper that groups the label, description, and errors of a form field. It passes id, aria-describedby, aria-invalid, and required to the input inside, such as UiInput.

Shown to other members.

Enter a full email address.

A native element receives the same attributes.

Code ​

vue
<script setup lang="ts">
import { shallowRef } from 'vue'
import { UiField, UiInput } from '@wing-land/ui'
const name = shallowRef('')
const email = shallowRef('ada@')
</script>
<template>
  <div class="demo">
    <UiField label="Name" description="Shown to other members." required>
      <UiInput v-model="name" autocomplete="name" />
    </UiField>
    <UiField
      label="Email"
      required
      required-indicator="asterisk"
      error="Enter a full email address."
    >
      <UiInput v-model="email" type="email" autocomplete="email" />
    </UiField>
    <UiField
      v-slot="field"
      label="Color"
      description="A native element receives the same attributes."
    >
      <input
        :id="field.id"
        type="color"
        :aria-describedby="field.describedBy"
        :aria-invalid="field.invalid || undefined"
      />
    </UiField>
  </div>
</template>
<style scoped>
.demo {
  max-width: 420px;
  padding: 1rem 0;
}
</style>

Usage ​

Put only one input inside each UiField. For native elements, pass the default slot props (id, describedBy, invalid, required, disabled) to the element. By default, required fields show the text "Required" next to the label. To use an asterisk instead, set the requiredIndicator prop on UiField, or requiredIndicator in the config to change every field.

Accessibility ​

The label is a <label> tied to the input. The description and errors are linked with aria-describedby, and the error state with aria-invalid. Errors are shown with an icon and text, not color alone. With group, the inputs are grouped in a <fieldset> with a <legend>.

API ​

Props

PropDefaultType
labelRequired

Label.

—string
description

Description below the label.

—string
error

Error message. Multiple messages are listed. Nothing is shown when empty.

—string | readonly string[] | null
required

Shows the required indicator and passes required to the input.

falseboolean
disabled

Disables the input.

falseboolean
id

id of the input. Generated when omitted. Set it when the error summary links to the input.

—string
group

Renders <fieldset> and <legend>. Use it to group several inputs, such as checkboxes. radiogroup also adds the required and error state to the group.

falseboolean | 'radiogroup'
hideLabel

Hides the label from the screen while keeping it for screen readers.

falseboolean
requiredIndicator

How required fields are marked. Defaults to the value in the config (text by default).

—UiRequiredIndicator

Slots

SlotSlot props
default

The input. Pass the slot props to native elements.

UiFieldSlotProps
label

Label content.

any
description

Description content.

any

UiFormErrorSummary ​

A list of errors placed at the top of a form after a failed submit. It takes focus when shown, and each item moves to the related input.

Code ​

vue
<script setup lang="ts">
import { shallowRef } from 'vue'
import { UiButton, UiField, UiFormErrorSummary, UiInput, useFormErrors } from '@wing-land/ui'
const name = shallowRef('')
const expiresAt = shallowRef('')
const form = useFormErrors({ ids: { expiresAt: 'token-expires' } })

function checkName() {
  if (!name.value.trim()) form.set({ ...form.errors.value, name: ['Enter a name.'] })
}

// Stands in for a request that answers with an ASP.NET Core ValidationProblem.
function submit() {
  const errors: Record<string, string[]> = {}
  if (!name.value.trim()) errors.name = ['Enter a name.']
  if (!expiresAt.value) errors.expiresAt = ['Choose when the token expires.']
  if (!form.setFromProblem({ status: 400, errors })) form.clear()
}
</script>
<template>
  <form class="demo" novalidate @submit.prevent="submit">
    <UiFormErrorSummary :errors="form.summary.value" />
    <UiField id="name" label="Name" required :error="form.get('name')">
      <UiInput v-model="name" @blur="checkName" @input="name.trim() && form.clear('name')" />
    </UiField>
    <UiField id="token-expires" label="Expires at" required :error="form.get('expiresAt')">
      <UiInput v-model="expiresAt" type="date" @input="form.clear('expiresAt')" />
    </UiField>
    <UiButton type="submit" variant="solid">Create token</UiButton>
  </form>
</template>
<style scoped>
.demo {
  display: grid;
  gap: 24px;
  justify-items: start;
  max-width: 420px;
  padding: 1rem 0;
}
.demo > :deep(.ui-field) {
  justify-self: stretch;
  margin: 0;
}
.demo > :deep(.ui-form-error-summary) {
  margin: 0;
}
</style>

Usage ​

Errors can be managed with useFormErrors(). Pass a response shaped like { errors: { name: ["…"] } }, such as ASP.NET Core's ValidationProblemDetails, to setFromProblem() to fill both the field errors and the summary.

ts
const form = useFormErrors({ ids: { expiresAt: 'token-expires' } })
const response = await fetch('/api/tokens', { method: 'POST', body })
if (!response.ok && !form.setFromProblem(await response.json())) showGeneralError()
// <UiFormErrorSummary :errors="form.summary.value" />
// <UiField id="name" :error="form.get('name')">…</UiField>

Each item links to the input whose id matches the field name. When the id differs, map it with ids. Once the user fixes an input, remove its error with clear(name).

Accessibility ​

Renders a <section> named by its heading. With autofocus, focus moves to the summary each time a new list of errors arrives, so screen reader users also notice that the submit failed.

API ​

Props

PropDefaultType
errorsRequired

Errors. { id?, message }. With id, the item links to that input.

—readonly UiFormError[]
title

Heading. Defaults to the locale text.

—string
headingLevel

Heading level.

22 | 3 | 4
autofocus

Moves focus to the summary when it receives a new list of errors.

trueboolean