Skip to content
intermediatePhase 39 · Accessibility

Accessible Forms

Build forms with proper labels, error messages, and ARIA attributes.

45m
0 problems
Topic Progress0%

Labels

Labels

Every form input needs a visible or accessible label.

Explicit Labels

// ✅ Best: Explicit label
<label htmlFor="email">Email</label>
<input id="email" type="email" />

// React
function EmailField() {
  return (
    <div>
      <label htmlFor="email">Email</label>
      <input id="email" type="email" name="email" />
    </div>
  );
}

Implicit Labels

// ✅ Good: Implicit label
<label>
  Email
  <input type="email" />
</label>

aria-label

// When visible label isn't possible
<input type="search" aria-label="Search products" />
<button aria-label="Close dialog">×</button>

aria-labelledby

// Reference visible text
<h2 id="billing-title">Billing Address</h2>
<form aria-labelledby="billing-title">
  {/* Form fields */}
</form>

// Multiple references
<span id="label">Name</span>
<span id="hint">First and last name</span>
<input
  aria-labelledby="label hint"
  aria-describedby="hint"
/>

Placeholder is NOT a Label

// ❌ Bad: Placeholder as label
<input placeholder="Email" />

// ✅ Good: Label + placeholder
<label htmlFor="email">Email</label>
<input id="email" placeholder="you@example.com" />

Input Types

// Use correct input types
<input type="email" /> {/* Shows email keyboard */}
<input type="tel" /> {/* Shows phone keyboard */}
<input type="url" /> {/* Shows URL keyboard */}
<input type="number" /> {/* Shows number keyboard */}

Error Messages

Error Messages

Accessible error announcement and display.

Error Pattern

function FormField({ label, error, id }) {
  const errorId = `${id}-error`;
  const hintId = `${id}-hint`;

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        aria-invalid={!!error}
        aria-describedby={error ? errorId : hintId}
      />
      <span id={hintId} className="hint">Help text</span>
      {error && (
        <span id={errorId} className="error" role="alert">
          {error}
        </span>
      )}
    </div>
  );
}

Error Summary

function Form({ errors, onSubmit }) {
  return (
    <form onSubmit={onSubmit}>
      {/* Error summary */}
      {Object.keys(errors).length > 0 && (
        <div role="alert" className="error-summary">
          <h2>Please fix the following errors:</h2>
          <ul>
            {Object.entries(errors).map(([field, error]) => (
              <li key={field}>
                <a href={`#${field}`}>{error}</a>
              </li>
            ))}
          </ul>
        </div>
      )}

      {/* Form fields */}
    </form>
  );
}

Live Error Announcements

function Form() {
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await submitForm();
    } catch (err) {
      setError('Form submission failed. Please try again.');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Visible errors */}
      {error && <div className="error" role="alert">{error}</div>}
      
      {/* Screen reader only errors */}
      <div aria-live="assertive" className="sr-only">
        {error}
      </div>
    </form>
  );
}
},
{
  "id": "ch3",
  "title": "ARIA Describedby",
  "content": "## ARIA Describedby

Provide additional context for form inputs.

Help Text

function PasswordField() {
  return (
    <div>
      <label htmlFor="password">Password</label>
      <input
        id="password"
        type="password"
        aria-describedby="password-hint"
      />
      <span id="password-hint" className="hint">
        Must be at least 8 characters with one uppercase letter
      </span>
    </div>
  );
}

Character Count

function TextareaField({ maxLength }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <label htmlFor="bio">Bio</label>
      <textarea
        id="bio"
        maxLength={maxLength}
        aria-describedby="bio-count"
        onChange={(e) => setCount(e.target.value.length)}
      />
      <span id="bio-count">
        {count} of {maxLength} characters
      </span>
    </div>
  );
}

Multiple Descriptions

function CreditCardField() {
  return (
    <div>
      <label htmlFor="cc">Credit Card</label>
      <input
        id="cc"
        type="text"
        aria-describedby="cc-hint cc-format"
      />
      <span id="cc-hint" className="hint">
        Enter your 16-digit card number
      </span>
      <span id="cc-format" className="format">
        Format: XXXX-XXXX-XXXX-XXXX
      </span>
    </div>
  );
}

Conditional Descriptions

function FormField({ label, error, hint, id }) {
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        aria-invalid={!!error}
        aria-describedby={
          [error && `${id}-error`, hint && `${id}-hint`]
            .filter(Boolean)
            .join(' ')
        }
      />
      {hint && <span id={`${id}-hint`} className="hint">{hint}</span>}
      {error && (
        <span id={`${id}-error`} className="error">
          {error}
        </span>
      )}
    </div>
  );
}

Form Validation Checklist

  • All inputs have labels
  • Required fields marked
  • Errors announced to screen readers
  • Help text provided
  • Error summary at top of form
  • Focus moves to error on submit

Practice Problems

0/3solved
Build Accessible Forms Component

Create a reusable React component implementing Accessible Forms. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
Accessible Forms Testing

Write unit and integration tests for Accessible Forms using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
Accessible Forms Performance

Optimize Accessible Forms for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

Quiz

1. Why not use placeholder as a label?

Question 1 options

2. What does aria-describedby do?

Question 2 options

3. What is the primary purpose of Accessible Forms?

Question 3 options

4. What is a common mistake when implementing Accessible Forms?

Question 4 options

Flashcards

Question

Why are form labels important?

Answer

They identify the purpose of inputs for screen readers and all users.

Question

How do you associate labels with inputs?

Answer

Using htmlFor/id attributes, wrapping input in label, or aria-label.

Question

What is aria-describedby?

Answer

An attribute that references elements providing additional help text for an input.

Question

How should form errors be displayed?

Answer

Visible error messages near inputs, announced to screen readers, with error summary at top.

Question

What is Accessible Forms?

Answer

Accessible Forms is a key concept in frontend development.

Revision Notes

Key Takeaways

  • 1.Every input needs a label (explicit, implicit, or aria-label)
  • 2.Errors should be announced to screen readers
  • 3.Use aria-describedby for help text and hints
  • 4.Provide error summary at top of form
  • 5.Never use placeholder as the only label

Interview Tips

  • Explain the importance of form labels
  • Discuss how to announce form errors
  • Know how to use aria-describedby

Cheat Sheet

Accessible Forms Cheat Sheet

Labels

  • Explicit:
  • Implicit:
  • aria-label when visible not possible

Errors

  • role="alert" for announcements
  • aria-invalid="true" on inputs
  • aria-describedby for error text

Help Text

  • aria-describedby for hints
  • Multiple descriptions with space-separated IDs

Checklist

  • All inputs have labels
  • Errors announced
  • Help text provided
  • Error summary at top