FormField

Pairs a control with its label, description and error.

Shown on your profile page

<script setup lang="ts">
  import { ref } from 'vue'
  import { FormField, Input } from '@hina-ui/vue'

  const name = ref('')
</script>

<template>
  <FormField label="Nickname" description="Shown on your profile page" required class="w-80">
    <Input v-model="name" placeholder="Hoshimi Shion" />
  </FormField>
</template>
vue

Usage

import { FormField } from '@hina-ui/vue'
ts

A form field lays out one control together with its label, description and error message. Inside a Form it reads the error of its field through name; on its own, error sets the message directly. Controls from the library connect automatically once placed in a field: the label points at the control, the description and the error are linked with aria-describedby, the control turns invalid while an error is present, and the field’s disabled is passed down.

<script setup lang="ts">
  import { ref } from 'vue'
  import { FormField, Input } from '@hina-ui/vue'

  const name = ref('')
</script>

<template>
  <FormField label="Nickname" class="w-80">
    <Input v-model="name" />
  </FormField>
</template>
vue

Examples

Horizontal layout

orientation="horizontal" puts the label on the leading side and the control on the trailing side. label-width accepts a CSS length or a number of pixels; it defaults to 10rem. Use FormLayout to share a label width across fields. The horizontal layout always keeps two columns.

Shown on your public profile.

<script setup lang="ts">
  import { ref } from 'vue'
  import { FormField, Input } from '@hina-ui/vue'
  const name = ref('Hina')
</script>
<template>
  <FormField
    label="Display name"
    orientation="horizontal"
    label-width="8rem"
    description="Shown on your public profile."
    class="w-full"
  >
    <Input v-model="name" />
  </FormField>
</template>
vue

Description placement

description-placement="label" places the description below the label and before the control in reading order. The default, control, places it below the control. Errors always stay in the control area. Both placements support the description slot and retain the same accessible association.

Shown on your public profile.

Use an address that can receive notifications.

Enter an email address.

<script setup lang="ts">
  import { ref } from 'vue'
  import { FormField, Input, Stack } from '@hina-ui/vue'
  const name = ref('Hina')
  const email = ref('')
</script>
<template>
  <Stack class="w-full">
    <FormField
      label="Display name"
      orientation="responsive"
      description-placement="label"
      description="Shown on your public profile."
    >
      <Input v-model="name" />
    </FormField>
    <FormField
      label="Email address"
      orientation="responsive"
      description-placement="control"
      description="Use an address that can receive notifications."
      error="Enter an email address."
    >
      <Input v-model="email" type="email" />
    </FormField>
  </Stack>
</template>
vue

Responsive layout

orientation="responsive" stacks content while the field is narrower than 32rem, and uses two columns at 32rem and above. This follows the field’s own available width, including inside a multi-column form, rather than the viewport. Resize the example to see the switch.

Resize this panel: fields stack below 32rem.

<script setup lang="ts">
  import { ref } from 'vue'
  import { FormField, Input, Stack } from '@hina-ui/vue'
  const name = ref('Hina')
</script>
<template>
  <Stack class="w-full max-w-2xl resize-x overflow-auto p-2">
    <FormField
      label="Display name"
      orientation="responsive"
      description="Resize this panel: fields stack below 32rem."
    >
      <Input v-model="name" />
    </FormField>
  </Stack>
</template>
vue

Description

description shows a line of help under the control; the slot of the same name takes richer content.

Shown on your profile page and editable any time

Up to 80 characters, line breaks allowed.

<script setup lang="ts">
  import { ref } from 'vue'
  import { FormField, Input, Stack, Textarea } from '@hina-ui/vue'

  const name = ref('')
  const bio = ref('')
</script>

<template>
  <Stack gap="md" align="stretch" class="w-80">
    <FormField label="Nickname" description="Shown on your profile page and editable any time">
      <Input v-model="name" />
    </FormField>
    <FormField label="Bio">
      <Textarea v-model="bio" />
      <template #description>Up to 80 characters, line breaks allowed.</template>
    </FormField>
  </Stack>
</template>
vue

Required mark

required shows a mark after the label and gives assistive technology the matching text. The mark is only a hint; whether the field is required is up to the validation rules.

<script setup lang="ts">
  import { ref } from 'vue'
  import { FormField, Input } from '@hina-ui/vue'

  const email = ref('')
</script>

<template>
  <FormField label="Email" required class="w-80">
    <Input v-model="email" type="email" />
  </FormField>
</template>
vue

Standalone

Outside a form, error decides the message directly, which suits places that run their own validation.

The email is not valid

<script setup lang="ts">
  import { ref } from 'vue'
  import { FormField, Input, Stack, Switch } from '@hina-ui/vue'

  const email = ref('shion@example')
  const failed = ref(true)
</script>

<template>
  <Stack gap="md" align="stretch" class="w-80">
    <Switch v-model="failed">Show the error</Switch>
    <FormField label="Email" :error="failed ? 'The email is not valid' : undefined">
      <Input v-model="email" type="email" />
    </FormField>
  </Stack>
</template>
vue

Kinds of controls

A single control is linked through the label’s for; grouped controls such as radio groups, checkbox groups, sliders, ratings and date fields are linked to the label through aria-labelledby.

mm
dd
yyyy
<script setup lang="ts">
  import { ref } from 'vue'
  import {
    CheckboxGroup,
    DateField,
    FormField,
    RadioGroup,
    Rating,
    Slider,
    Stack,
  } from '@hina-ui/vue'

  const kinds = [
    { label: 'Game', value: 'game' },
    { label: 'Novel', value: 'novel' },
    { label: 'Manga', value: 'manga' },
  ]
  const kind = ref<string | number | null>('game')
  const tags = ref<Array<string | number>>([])
  const score = ref(4)
  const volume = ref(30)
  const released = ref<string | null>(null)
</script>

<template>
  <Stack gap="md" align="stretch" class="w-80">
    <FormField label="Kind">
      <RadioGroup v-model="kind" :options="kinds" orientation="horizontal" />
    </FormField>
    <FormField label="Tags">
      <CheckboxGroup v-model="tags" :options="kinds" orientation="horizontal" />
    </FormField>
    <FormField label="Score">
      <Rating v-model="score" />
    </FormField>
    <FormField label="Volume">
      <Slider v-model="volume" />
    </FormField>
    <FormField label="Release date">
      <DateField v-model="released" />
    </FormField>
  </Stack>
</template>
vue

Behavior

  • A field holds one control; an id or aria-describedby set on the control wins over the values the field generates.
  • The error message pushes the content below it aside as it appears and pulls back as it leaves; height and spacing change continuously.
  • When the field loses focus it notifies its form, which the blur validation timing relies on.

Accessibility

  • The label points at the control through for; grouped controls link back to the label through aria-labelledby.
  • The required mark is hidden from assistive technology, and hidden text says the field is required.
  • The error message carries aria-live="polite", so it is announced when it appears.

API

Props

Prop
Type
Default
Description
name
string
—
Field name used to read the error from the form
label
string
—
Label text
description
string
—
Description text
error
string
—
Error text set directly
required
boolean
false
Whether the required mark shows
orientation
'vertical' | 'horizontal' | 'responsive'
'vertical'
Label/control layout; inherits from FormLayout
descriptionPlacement
'label' | 'control'
'control'
Description below the label or control; inherits from FormLayout
labelWidth
string | number
'10rem'
Label column width: CSS length or pixels; inherits from FormLayout
disabled
boolean
false
Whether the control is disabled
class
string
—
Classes appended to the root element

Slots

Slot
Description
default
The control
label
Label content
description
Description content