Forms

Forms allow users to enter data and interact with the system. Standard provides a complete set of form controls including inputs, textareas, selects, checkboxes, radio buttons, switches, and file uploads — all with built-in validation states and accessibility.


Installation

Copy the form CSS from styles/docs.css or include the Standard stylesheet:

<link rel="stylesheet" href="standard.min.css">

Then use form classes in your HTML:

<div class="FormField">
    <label class="FormField-label" for="email">Email</label>
    <input type="email" id="email" class="Input" placeholder="you@example.com">
</div>

Usage

The base .Input class provides core input styling. Wrap inputs in .FormField for proper labeling and spacing.


Input Variants

Sizes

Inputs come in three sizes: small, medium (default), and large.

With Icons

Add leading or trailing icons for visual context.

With Button

Common pattern for search or subscribe fields.

Password with Toggle

Toggle password visibility for user convenience.


States

Validation States

Visual feedback for different input states.

This field is required
Looks good!

Helper Text & Character Count

Provide additional context or show remaining characters.

We'll never share your email.

Required Fields

Indicate required fields with an asterisk.


Textarea

Multi-line text input for longer content.


Select

Dropdown for choosing from predefined options.


Checkbox

For binary choices or multiple selections.

Checkbox Group

Group related checkboxes in a fieldset.

Interests

Indeterminate State

For “select all” patterns where some items are selected.


Radio Buttons

For single selection from a list of options.

Plan

Radio Cards

Richer radio selection with descriptions and prices.


Switch

Toggle switches for on/off settings.

Switch with Description

For settings that need more context.

Switch Sizes


File Upload

File input with drag-and-drop support.

File Upload with Preview

Show a preview thumbnail alongside the upload button.

Preview
JPG, PNG or GIF. Max 2MB.

Multiple File Upload

Upload multiple files with a file list display.

document.pdf 2.4 MB
photo.jpg 1.1 MB

Form Layouts

Stacked Form (Default)

Vertical layout with labels above inputs.

Inline Form

Horizontal layout for compact forms like newsletter signups.

Two-Column Form

Side-by-side fields for related data.


Common Patterns

Login Form

Welcome back

Sign in to your account

Forgot password?
Don't have an account? Sign up

Signup Form

Create your account

Start your 14-day free trial

Must be at least 8 characters with a number and symbol

Settings Form

Profile Information
Notifications

Contact Form

Get in touch

Form Validation

Please enter a valid email address
Strong password

Customization

Override form styles using CSS custom properties:

/* Custom input styling */
.Input {
  --input-radius: var(--space-2);
  --input-border: var(--bd-s);
  --input-focus: var(--accent);
  
  border-radius: var(--input-radius);
  border-color: var(--input-border);
}

.Input:focus {
  border-color: var(--input-focus);
  box-shadow: 0 0 0 2px oklch(60% 0.15 250 / 0.15);
}

/* Custom checkbox color */
.Checkbox-input:checked + .Checkbox-box {
  background-color: oklch(55% 0.2 150); /* Green */
  border-color: oklch(55% 0.2 150);
}

/* Custom switch color */
.Switch-input:checked + .Switch-track {
  background-color: oklch(55% 0.2 150);
  border-color: oklch(55% 0.2 150);
}

/* Pill-shaped inputs */
.Input--pill {
  border-radius: 999px;
  padding-left: var(--space-4);
  padding-right: var(--space-4);
}

/* Custom error color */
.Input--error {
  border-color: oklch(55% 0.25 0); /* Deeper red */
}

API Reference

Form Field Classes

Class Description
.FormField Container for label, input, and helper text
.FormField-label Label styling for form fields
.FormField-label--required Adds asterisk to indicate required field
.FormField-helper Helper text below input
.FormField-message Validation message container
.FormField-message--error Error message styling (red)
.FormField-message--success Success message styling (green)
.FormField-footer Container for helper + count
.FormField-count Character count display

Input Classes

Class Description
.Input Base input styling (required)
.Input--small Small input size
.Input--large Large input size
.Input--error Error state (red border)
.Input--success Success state (green border)
.Input--withIcon Padding for leading icon
.Input--withIconTrailing Padding for trailing icon
.Input--grouped For input + button combos
.Input-wrapper Container for input with icon
.Input-icon Icon inside input wrapper
.Input-icon--trailing Position icon on right
.Input-toggle Password visibility toggle button
.Input-group Container for input + button

Textarea Classes

Class Description
.Textarea Base textarea styling
.Textarea--error Error state (red border)

Select Classes

Class Description
.Select Base select/dropdown styling

Checkbox Classes

Class Description
.Checkbox Container for custom checkbox
.Checkbox-input Hidden native checkbox
.Checkbox-box Custom checkbox visual
.Checkbox-box--indeterminate Indeterminate state styling
.Checkbox-label Label text for checkbox
.Checkbox--disabled Disabled state
.CheckboxGroup Container for multiple checkboxes

Radio Classes

Class Description
.Radio Container for custom radio
.Radio-input Hidden native radio
.Radio-circle Custom radio visual
.Radio-label Label text for radio
.RadioGroup Container for radio group
.RadioCardGroup Container for radio cards
.RadioCard Rich radio option with description
.RadioCard--selected Selected radio card state

Switch Classes

Class Description
.Switch Container for toggle switch
.Switch-input Hidden native checkbox
.Switch-track Switch track and thumb
.Switch-label Label text for switch
.Switch--block Full-width switch with border
.Switch--small Small switch size
.Switch--large Large switch size
.Switch--disabled Disabled state
.Switch-content Container for label + description
.Switch-description Secondary text under label

File Upload Classes

Class Description
.FileInput File upload container
.FileInput-input Hidden native file input
.FileInput-label Styled drop zone label
.FileInput-icon Upload icon
.FileInput-text Main drop zone text
.FileInput-hint File type/size hint
.FileInput--with-preview Layout for preview thumbnail
.FileInput--multiple Multiple file upload variant
.FileInput-list Container for uploaded file list
.FileInput-item Individual file item

Layout Classes

Class Description
.FormInline Horizontal form layout
.FormRow Two-column grid row
.FormFieldset Fieldset without default styling
.FormFieldset-legend Fieldset legend styling

CSS Reference

/* Form Field */
.FormField {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.FormField-label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--fg);
}

.FormField-label--required::after {
  content: " *";
  color: oklch(55% 0.2 25);
}

.FormField-helper {
  font-size: 0.8rem;
  color: var(--fg-3);
}

.FormField-message {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: 0.8rem;
}

.FormField-message--error {
  color: oklch(55% 0.2 25);
}

.FormField-message--success {
  color: oklch(55% 0.15 150);
}

.FormField-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.FormField-count {
  font-size: 0.8rem;
  color: var(--fg-3);
}

/* Input */
.Input {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  font-size: 0.9rem;
  border: 1px solid var(--bd-s);
  border-radius: var(--r-s);
  background: var(--bg);
  color: var(--fg);
  transition: border-color 0.15s, box-shadow 0.15s;
}

.Input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px oklch(60% 0.15 250 / 0.15);
}

.Input::placeholder {
  color: var(--fg-3);
}

.Input:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  background: var(--bg-s);
}

/* Input Sizes */
.Input--small {
  padding: var(--space-1) var(--space-2);
  font-size: 0.8rem;
}

.Input--large {
  padding: var(--space-3) var(--space-4);
  font-size: 1rem;
}

/* Input States */
.Input--error {
  border-color: oklch(55% 0.2 25);
}

.Input--error:focus {
  box-shadow: 0 0 0 2px oklch(55% 0.2 25 / 0.15);
}

.Input--success {
  border-color: oklch(55% 0.15 150);
}

.Input--success:focus {
  box-shadow: 0 0 0 2px oklch(55% 0.15 150 / 0.15);
}

/* Input with Icons */
.Input-wrapper {
  position: relative;
  display: flex;
  align-items: center;
}

.Input-icon {
  position: absolute;
  left: var(--space-3);
  color: var(--fg-3);
  pointer-events: none;
}

.Input-icon--trailing {
  left: auto;
  right: var(--space-3);
}

.Input--withIcon {
  padding-left: calc(var(--space-3) + 1.25rem + var(--space-2));
}

.Input--withIconTrailing {
  padding-right: calc(var(--space-3) + 1.25rem + var(--space-2));
}

.Input-toggle {
  position: absolute;
  right: var(--space-2);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--fg-3);
  padding: var(--space-1);
}

/* Input Group */
.Input-group {
  display: flex;
}

.Input--grouped {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  border-right: none;
}

.Input-group .Button {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

/* Textarea */
.Textarea {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  font-size: 0.9rem;
  border: 1px solid var(--bd-s);
  border-radius: var(--r-s);
  background: var(--bg);
  color: var(--fg);
  resize: vertical;
  min-height: 80px;
  font-family: inherit;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.Textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px oklch(60% 0.15 250 / 0.15);
}

/* Select */
.Select {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  font-size: 0.9rem;
  border: 1px solid var(--bd-s);
  border-radius: var(--r-s);
  background: var(--bg);
  color: var(--fg);
  appearance: none;
  background-image: url("data:image/svg+xml,...");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  cursor: pointer;
}

/* Checkbox */
.Checkbox {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
}

.Checkbox-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.Checkbox-box {
  width: 18px;
  height: 18px;
  border: 2px solid var(--bd-s);
  border-radius: var(--r-xs);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.15s, border-color 0.15s;
  flex-shrink: 0;
}

.Checkbox-input:checked + .Checkbox-box {
  background-color: var(--accent);
  border-color: var(--accent);
}

.Checkbox-input:focus-visible + .Checkbox-box {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.Checkbox--disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.Checkbox-label {
  font-size: 0.9rem;
}

/* Radio */
.Radio {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
}

.Radio-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.Radio-circle {
  width: 18px;
  height: 18px;
  border: 2px solid var(--bd-s);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.15s;
  flex-shrink: 0;
}

.Radio-input:checked + .Radio-circle {
  border-color: var(--accent);
}

.Radio-input:checked + .Radio-circle::after {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
}

.RadioGroup {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Radio Card */
.RadioCardGroup {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.RadioCard {
  display: flex;
  padding: var(--space-4);
  border: 1px solid var(--bd);
  border-radius: var(--r-m);
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.RadioCard--selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

/* Switch */
.Switch {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
}

.Switch-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.Switch-track {
  width: 40px;
  height: 22px;
  background: var(--bg-s);
  border: 1px solid var(--bd-s);
  border-radius: 11px;
  position: relative;
  transition: background-color 0.2s, border-color 0.2s;
  flex-shrink: 0;
}

.Switch-track::after {
  content: "";
  position: absolute;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: white;
  top: 2px;
  left: 2px;
  transition: transform 0.2s;
}

.Switch-input:checked + .Switch-track {
  background: var(--accent);
  border-color: var(--accent);
}

.Switch-input:checked + .Switch-track::after {
  transform: translateX(18px);
}

.Switch--small .Switch-track { width: 32px; height: 18px; }
.Switch--large .Switch-track { width: 48px; height: 26px; }

.Switch--disabled { opacity: 0.5; cursor: not-allowed; }

.Switch--block {
  display: flex;
  justify-content: space-between;
  padding: var(--space-3);
  border: 1px solid var(--bd);
  border-radius: var(--r-m);
}

.Switch-content { display: flex; flex-direction: column; gap: var(--space-1); }
.Switch-description { font-size: 0.8rem; color: var(--fg-3); }

/* File Input */
.FileInput {
  position: relative;
}

.FileInput-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.FileInput-label {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-6);
  border: 2px dashed var(--bd);
  border-radius: var(--r-m);
  cursor: pointer;
  text-align: center;
  transition: border-color 0.15s, background 0.15s;
}

.FileInput-label:hover {
  border-color: var(--accent);
  background: oklch(60% 0.15 250 / 0.05);
}

.FileInput-icon { font-size: 1.5rem; color: var(--fg-3); }
.FileInput-text { font-weight: 500; }
.FileInput-hint { font-size: 0.8rem; color: var(--fg-3); }

.FileInput-list { display: flex; flex-direction: column; gap: var(--space-2); }
.FileInput-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border: 1px solid var(--bd);
  border-radius: var(--r-s);
}

/* Form Layouts */
.FormInline {
  display: flex;
  gap: var(--space-3);
  align-items: flex-end;
}

.FormRow {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
}

.FormFieldset {
  border: none;
  padding: 0;
  margin: 0;
}

.FormFieldset-legend {
  font-weight: 600;
  margin-bottom: var(--space-3);
}

Accessibility

Labels

Always associate labels with inputs using for/id or by wrapping.

<!-- Method 1: for/id association -->
<label for="email-field">Email</label>
<input type="email" id="email-field" class="Input">

<!-- Method 2: Wrap input in label -->
<label class="Checkbox">
    <input type="checkbox">
    <span class="Checkbox-label">Accept terms</span>
</label>

Error Messages

Connect error messages to inputs with aria-describedby.

<input 
    type="email" 
    id="email" 
    class="Input Input--error" 
    aria-invalid="true"
    aria-describedby="email-error"
>
<span id="email-error" class="FormField-message FormField-message--error">
    Please enter a valid email
</span>

Required Fields

Mark required fields programmatically.

<label for="name">
    Name <span aria-hidden="true">*</span>
</label>
<input type="text" id="name" required aria-required="true">

Fieldsets for Groups

Use fieldsets to group related fields.

<fieldset>
    <legend>Shipping Address</legend>
    <!-- Address fields -->
</fieldset>

Keyboard Support

Key Action
Tab Move focus to next field
Shift + Tab Move focus to previous field
Space Toggle checkbox/switch, select radio
Arrow keys Navigate between radio options
Enter Submit form (when on button)

Best Practices

Do

  • Use clear, descriptive labels — “Email address” not “Input 1”
  • Group related fields — Use fieldsets for address, payment info
  • Show validation inline — Error messages next to the field
  • Indicate required fields — Asterisk or “(required)” text
  • Provide helper text — Format hints, password requirements
  • Use appropriate input typestype="email", type="tel", etc.
  • Size touch targets — Minimum 44px for mobile

Don’t

  • Use placeholder as label — Placeholders disappear on focus
  • Disable submit without explanation — Show why it’s disabled
  • Validate on every keystroke — Wait for blur or submit
  • Use only color for errors — Add icons and text
  • Reset forms on error — Preserve user input
  • Hide password requirements — Show them upfront