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">
We’ll 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()
.