Forms
Form patterns guide users through data entry with clear feedback and minimal friction. Every interaction follows Canon tokens for consistent, accessible experiences.
The Problem
Forms are points of friction. Users abandon forms that feel overwhelming, confusing, or broken. Good form design reduces cognitive load while ensuring data validity.
Overwhelm
Too many fields visible at once causes decision fatigue.
Unclear Expectations
Missing labels, placeholders, or help text leave users guessing.
Late Validation
Discovering errors only after submission frustrates users.
Poor Error Recovery
Vague error messages don't help users fix problems.
Input Fields
All form inputs follow a consistent structure: label, input, description, and error state.
TextField
Standard text input with label, placeholder, and helper text.
As it appears on your ID.
"token attr-name">class="token tag"><script "token attr-name">lang="ts">
import { TextField } from '@create-something/components';
let name = $state('');
"token attr-name">class="token tag"></script>
"token attr-name">class="token tag"><TextField
bind:value={name}
"token attr-name">label="Full Name"
"token attr-name">placeholder="Enter your full name"
"token attr-name">description="As it appears on your ID."
required
/>Error State
Error styling with descriptive message helps users recover.
Please enter a valid email address.
"token attr-name">class="token tag"><TextField
bind:value={email}
"token attr-name">label="Email Address"
"token attr-name">type="email"
required
"token attr-name">error={emailError}
/>
<!-- CSS for error state -->
"token attr-name">class="token tag"><style>
.has-error .textfield-input {
border-color: var(--color-error);
}
.has-error .textfield-input:focus {
box-shadow: 0 0 0 3px var(--color-error-muted);
}
.textfield-error {
font-size: var(--text-caption);
color: var(--color-error);
}
"token attr-name">class="token tag"></style>Size Variants
Small, medium (default), and large sizes for different contexts.
"token attr-name">class="token tag"><TextField "token attr-name">label="Small" "token attr-name">size="sm" "token attr-name">placeholder="Small input" />
"token attr-name">class="token tag"><TextField "token attr-name">label="Medium" "token attr-name">size="md" "token attr-name">placeholder="Medium input" />
"token attr-name">class="token tag"><TextField "token attr-name">label="Large" "token attr-name">size="lg" "token attr-name">placeholder="Large input" />Selection Controls
Checkboxes, radio buttons, and switches for selecting options.
Checkbox
For binary choices or selecting multiple options from a set.
"token attr-name">class="token tag"><script "token attr-name">lang="ts">
import { Checkbox, CheckboxGroup } from '@create-something/components';
let notifications = $state(['email']);
"token attr-name">class="token tag"></script>
"token attr-name">class="token tag"><CheckboxGroup
"token attr-name">label="Notification Preferences"
bind:value={notifications}
"token attr-name">options={[
{ value: 'email', label: 'Email notifications' },
{ value: 'sms', label: 'SMS notifications' },
{ value: 'push', label: 'Push notifications', disabled: true }
]}
/>Radio Group
For selecting exactly one option from a mutually exclusive set.
"token attr-name">class="token tag"><RadioGroup
"token attr-name">label="Subscription Plan"
bind:value={plan}
"token attr-name">options={[
{ value: 'free', label: 'Free', description: 'Basic features' },
{ value: 'pro', label: 'Pro', description: 'All features' },
{ value: 'enterprise', label: 'Enterprise', description: 'Custom' }
]}
/>Switch
For toggling a setting on or off with immediate effect.
"token attr-name">class="token tag"><Switch
bind:checked={darkMode}
"token attr-name">label="Dark mode"
"token attr-name">onchange={(checked) => updateTheme(checked)}
/>Form Layout
Layout patterns organize form fields for clarity and flow.
Stacked Layout
Default vertical layout. Each field gets full width.
"token attr-name">class="token tag"><FormLayout
"token attr-name">title="Contact Information"
"token attr-name">description="We'll use this to get in touch."
"token attr-name">onsubmit={handleSubmit}
>
"token attr-name">class="token tag"><TextField "token attr-name">label="Name" "token attr-name">name="name" />
"token attr-name">class="token tag"><TextField "token attr-name">label="Email" "token attr-name">name="email" "token attr-name">type="email" />
"token attr-name">class="token tag"><TextArea "token attr-name">label="Message" "token attr-name">name="message" "token attr-name">rows={3} />
{#snippet actions()}
"token attr-name">class="token tag"><Button "token attr-name">variant="ghost">Cancel"token attr-name">class="token tag"></Button>
"token attr-name">class="token tag"><Button "token attr-name">type="submit">Submit"token attr-name">class="token tag"></Button>
{/snippet}
"token attr-name">class="token tag"></FormLayout>Two-Column Layout
For forms with many short fields. Collapses to single column on mobile.
"token attr-name">class="token tag"><FormLayout "token attr-name">variant="two-column">
"token attr-name">class="token tag"><TextField "token attr-name">label="First Name" "token attr-name">name="firstName" />
"token attr-name">class="token tag"><TextField "token attr-name">label="Last Name" "token attr-name">name="lastName" />
"token attr-name">class="token tag"><TextField "token attr-name">label="City" "token attr-name">name="city" />
"token attr-name">class="token tag"><TextField "token attr-name">label="Postal Code" "token attr-name">name="postal" />
"token attr-name">class="token tag"></FormLayout>Validation Patterns
Validate early and provide clear, actionable feedback.
Validate on Blur
Check field validity when the user leaves the field. Don't interrupt while typing.
onblur={(e) => {
"token keyword">if (!e.currentTarget.validity.valid) {
error = 'Invalid email';
} "token keyword">else {
error = null;
}
}}Validate on Submit
Always validate the entire form before submission. Show all errors at once.
onsubmit={(e) => {
e.preventDefault();
"token keyword">const errors = validateForm(formData);
"token keyword">if (Object.keys(errors).length) {
setErrors(errors);
"token keyword">return;
}
submit(formData);
}}Clear on Fix
Remove error state as soon as the user corrects the problem.
oninput={(e) => {
"token keyword">if (error && e.currentTarget.validity.valid) {
error = null;
}
}}Multi-Step Forms
Break long forms into manageable steps with clear progress indication.
Step Indicator
Shows current position and allows navigation to completed steps.
"token attr-name">class="token tag"><script "token attr-name">lang="ts">
import { MultiStepForm, FormStep } from '@create-something/components';
let currentStep = 1;
const steps = [
{ id: 1, label: 'Account', complete: true },
{ id: 2, label: 'Profile', complete: false },
{ id: 3, label: 'Preferences', complete: false },
{ id: 4, label: 'Confirm', complete: false }
];
"token attr-name">class="token tag"></script>
"token attr-name">class="token tag"><MultiStepForm {steps} bind:currentStep>
"token attr-name">class="token tag"><FormStep "token attr-name">step={1}>
"token attr-name">class="token tag"><TextField "token attr-name">label="Email" "token attr-name">name="email" />
"token attr-name">class="token tag"><TextField "token attr-name">label="Password" "token attr-name">name="password" "token attr-name">type="password" />
"token attr-name">class="token tag"></FormStep>
"token attr-name">class="token tag"><FormStep "token attr-name">step={2}>
"token attr-name">class="token tag"><TextField "token attr-name">label="Display Name" "token attr-name">name="displayName" />
"token attr-name">class="token tag"><TextArea "token attr-name">label="Bio" "token attr-name">name="bio" />
"token attr-name">class="token tag"></FormStep>
<!-- ... more steps ... -->
"token attr-name">class="token tag"></MultiStepForm>Accessibility
Forms must be usable by everyone, including keyboard and screen reader users.
Labels
- Every input needs a visible
<label> - Use
forattribute to associate label with input - Don't rely on placeholder as the only label
Error Messages
- Use
aria-invalid="true"on invalid inputs - Link errors with
aria-describedby - Use
role="alert"for dynamic errors
Required Fields
- Use
aria-required="true" - Visual indicator (asterisk) with hidden label text
- Explain format at form start, not per field
Focus Management
- Visible focus indicators (3px solid)
- Focus first error on validation failure
- Announce success after submission
Token Reference
Form patterns use these Canon design tokens.
--color-bg-elevated Input background color--color-border-default Default input border--color-border-emphasis Hover and focus border--color-focus Focus ring color (3px solid)--color-error Error text and border--color-error-muted Error focus ring background--space-xs / --space-sm / --space-md Field spacing and padding--radius-md Input border radius--duration-micro Border and focus transitions (200ms)Anti-Patterns
Common form mistakes to avoid.
Placeholder-only Labels
Placeholders disappear when the user types, leaving no context. Always use visible labels.
Blocking Validation
Don't prevent typing while validating. Show errors after blur, not during input.
Vague Error Messages
"Invalid input" doesn't help. Be specific: "Password must be at least 8 characters."
Required Asterisks Only
Asterisks need explanation. Add "* Required" legend or use aria-required.
Tiny Touch Targets
Inputs must be at least 44px tall for mobile. Use min-height: 44px.
No Loading State
Show a spinner or disable the submit button during submission to prevent double-submit.