Skip to main content

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.

"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.

Subscription Plan
"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.

Contact Information

We'll use this to get in touch.

"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 for attribute 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.