You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

4.8 KiB

Forms & Inputs

Contents

  • Forms use FieldGroup + Field
  • InputGroup requires InputGroupInput/InputGroupTextarea
  • Buttons inside inputs use InputGroup + InputGroupAddon
  • Option sets (2–7 choices) use ToggleGroup
  • FieldSet + FieldLegend for grouping related fields
  • Field validation and disabled states

Forms use FieldGroup + Field

Always use FieldGroup + Field — never raw div with space-y-*:

<FieldGroup>
  <Field>
    <FieldLabel for="email">Email</FieldLabel>
    <Input id="email" type="email" />
  </Field>
  <Field>
    <FieldLabel for="password">Password</FieldLabel>
    <Input id="password" type="password" />
  </Field>
</FieldGroup>

Use Field orientation="horizontal" for settings pages. Use FieldLabel class="sr-only" for visually hidden labels.

Choosing form controls:

  • Simple text input → Input
  • Dropdown with predefined options → Select
  • Searchable dropdown → Combobox
  • Native HTML select (no JS) → native-select
  • Boolean toggle → Switch (for settings) or Checkbox (for forms)
  • Single choice from few options → RadioGroup
  • Toggle between 2–7 options → ToggleGroup + ToggleGroupItem
  • OTP/verification code → InputOTP
  • Multi-line text → Textarea

InputGroup requires InputGroupInput/InputGroupTextarea

Never use raw Input or Textarea inside an InputGroup.

Incorrect:

<InputGroup>
  <Input placeholder="Search..." />
</InputGroup>

Correct:

<script setup lang="ts">
import { InputGroup, InputGroupInput } from "@/components/ui/input-group"
</script>

<template>
  <InputGroup>
    <InputGroupInput placeholder="Search..." />
  </InputGroup>
</template>

Buttons inside inputs use InputGroup + InputGroupAddon

Never place a Button directly inside or adjacent to an Input with custom positioning.

Incorrect:

<div class="relative">
  <Input placeholder="Search..." class="pr-10" />
  <Button class="absolute right-0 top-0" size="icon">
    <SearchIcon />
  </Button>
</div>

Correct:

<script setup lang="ts">
import { InputGroup, InputGroupInput, InputGroupAddon } from "@/components/ui/input-group"
</script>

<template>
  <InputGroup>
    <InputGroupInput placeholder="Search..." />
    <InputGroupAddon>
      <Button size="icon">
        <SearchIcon data-icon="inline-start" />
      </Button>
    </InputGroupAddon>
  </InputGroup>
</template>

Option sets (2–7 choices) use ToggleGroup

Don't manually loop Button components with active state.

Incorrect:

<script setup lang="ts">
const selected = ref("daily")
const options = ["daily", "weekly", "monthly"]
</script>

<template>
  <div class="flex gap-2">
    <Button
      v-for="option in options"
      :key="option"
      :variant="selected === option ? 'default' : 'outline'"
      @click="selected = option"
    >
      {{ option }}
    </Button>
  </div>
</template>

Correct:

<script setup lang="ts">
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
</script>

<template>
  <ToggleGroup spacing="2">
    <ToggleGroupItem value="daily">Daily</ToggleGroupItem>
    <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
    <ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
  </ToggleGroup>
</template>

Combine with Field for labelled toggle groups:

<Field orientation="horizontal">
  <FieldTitle id="theme-label">Theme</FieldTitle>
  <ToggleGroup aria-labelledby="theme-label" spacing="2">
    <ToggleGroupItem value="light">Light</ToggleGroupItem>
    <ToggleGroupItem value="dark">Dark</ToggleGroupItem>
    <ToggleGroupItem value="system">System</ToggleGroupItem>
  </ToggleGroup>
</Field>

Use FieldSet + FieldLegend for related checkboxes, radios, or switches — not div with a heading:

<FieldSet>
  <FieldLegend variant="label">Preferences</FieldLegend>
  <FieldDescription>Select all that apply.</FieldDescription>
  <FieldGroup class="gap-3">
    <Field orientation="horizontal">
      <Checkbox id="dark" />
      <FieldLabel for="dark" class="font-normal">Dark mode</FieldLabel>
    </Field>
  </FieldGroup>
</FieldSet>

Field validation and disabled states

Both attributes are needed — data-invalid/data-disabled styles the field (label, description), while aria-invalid/disabled styles the control.

<!-- Invalid. -->
<Field data-invalid>
  <FieldLabel for="email">Email</FieldLabel>
  <Input id="email" aria-invalid />
  <FieldDescription>Invalid email address.</FieldDescription>
</Field>

<!-- Disabled. -->
<Field data-disabled>
  <FieldLabel for="email">Email</FieldLabel>
  <Input id="email" disabled />
</Field>

Works for all controls: Input, Textarea, Select, Checkbox, RadioGroupItem, Switch, Slider, NativeSelect, InputOTP.