Overview

Input components for forms with consistent spacing, tokens, and dark mode. This page covers the building blocks we ship (Input, Textarea, Label, Select, Checkbox, Switch, Slider, Toggle/ToggleGroup) and how to wire them accessibly. App-level helpers like PasswordInput, SearchInput, and SubmitButton are referenced where useful.

Components

  • Input — single-line text, email, number, file, etc.
  • Textarea — multi-line text.
  • Label — associates a caption via htmlFor.
  • Select — Radix Select (trigger, content, items) with keyboard a11y.
  • Checkbox — boolean form control.
  • Switch — on/off toggle for settings.
  • Slider — range selection.
  • Toggle / ToggleGroup — two-state buttons, single or multiple selection.

Field pattern

Use the same pattern everywhere: Label, the control, optional description, optional error. Connect description/error via aria-describedby and mark invalid fields with aria-invalid. The Input supports a visual invalid variant for error styling.

'use client';

import * as React from 'react';
import { Input } from '@/components/ui/Input';
import { Label } from '@/components/ui/Label';

export function EmailField({ error }: { error?: string }) {
  const descId = 'email-desc';
  const errId = 'email-err';

  return (
    <div className="grid gap-2">
      <Label htmlFor="email">Email</Label>
      <Input
        id="email"
        type="email"
        placeholder="you@example.com"
        aria-describedby={error ? `${descId} ${errId}` : descId}
        aria-invalid={!!error}
        invalid={!!error}
      />
      <p id={descId} className="text-xs text-secondary-dark/70 dark:text-neutral">
        Well never share your email.
      </p>
      {error && (
        <p id={errId} className="text-xs text-destructive">
          {error}
        </p>
      )}
    </div>
  );
}

Select

The Select is a Radix component with trigger/content/items. It’s fully keyboard-accessible. Use a Label and ensure the trigger has an accessible name; keep the placeholder concise.

'use client';

import * as React from 'react';
import { Label } from '@/components/ui/Label';
import {
  Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
} from '@/components/ui/Select';

export function IndustrySelect() {
  return (
    <div className="grid gap-2">
      <Label htmlFor="industry">Industry</Label>
      <Select>
        <SelectTrigger id="industry" aria-label="Industry">
          <SelectValue placeholder="Select industry…" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="saas">SaaS</SelectItem>
          <SelectItem value="ecommerce">E-commerce</SelectItem>
          <SelectItem value="education">Education</SelectItem>
        </SelectContent>
      </Select>
    </div>
  );
}

Checkbox and Switch

  • Checkbox is for boolean form values.
  • Switch is a settings toggle. If you need to submit it with a form, mirror its state to a hidden input.
'use client';

import * as React from 'react';
import { Checkbox } from '@/components/ui/Checkbox';
import { Switch } from '@/components/ui/Switch';
import { Label } from '@/components/ui/Label';

export function BooleanExamples() {
  const [newsletter, setNewsletter] = React.useState(false);
  const [darkMode, setDarkMode] = React.useState(true);

  return (
    <div className="grid gap-4">
      {/* Checkbox as a form boolean */}
      <label className="flex items-center gap-2">
        <Checkbox
          checked={newsletter}
          onCheckedChange={(v) => setNewsletter(Boolean(v))}
          aria-label="Subscribe to newsletter"
        />
        <span>Subscribe to newsletter</span>
      </label>

      {/* Switch as a setting (with hidden input if inside a form) */}
      <div className="flex items-center justify-between">
        <Label htmlFor="darkmode">Dark mode</Label>
        <Switch id="darkmode" checked={darkMode} onCheckedChange={setDarkMode} />
        {/* <input type="hidden" name="darkMode" value={String(darkMode)} /> */}
      </div>
    </div>
  );
}

Slider and Toggle

Use the Slider for numeric ranges and Toggle / ToggleGroup for on/off or segmented choices. Keep labels visible and ensure focus rings are intact.

Validation and submit

Validate on the server (zod or your schema) and return { field: message }. In the client, place messages under controls, set aria-invalid, and link via aria-describedby. For submit flows inside <form>, prefer SubmitButton (reads useFormStatus()); for click-driven async use TransitionButton with useTransition().

References