Building Accessible Forms in SvelteKit: A Complete Guide

Why Form Accessibility Matters

Forms are where users give you their data — their name, email, payment details, preferences. If a form is inaccessible, you’re not just failing a compliance checkbox — you’re blocking real people from using your product. Screen reader users, keyboard-only navigators, users with motor impairments, and users with cognitive disabilities all interact with forms differently.

The good news: accessible forms aren’t harder to build. They’re just more intentional. Most accessibility wins come from using semantic HTML correctly, which also makes your code cleaner.

1. Every Input Needs a Label

The most common accessibility failure in forms: inputs without associated labels. Screen readers can’t announce what a field is for if there’s no label element linked to it.

The Right Way

<label for="email">Email Address</label>
<input id="email" name="email" type="email" />

The for attribute on the label matches the id on the input. This creates a programmatic association that screen readers use. It also makes the label clickable — clicking “Email Address” focuses the input, which helps users with motor impairments.

Common Mistakes

<!-- Bad: placeholder instead of label -->
<input placeholder="Email" />

<!-- Bad: label exists but no for/id link -->
<label>Email</label>
<input name="email" />

<!-- Bad: aria-label instead of visible label -->
<input aria-label="Email" name="email" />

Placeholders disappear when you start typing — they’re not labels. Unlinked labels and aria-label aren’t clickable and may not be announced in all screen readers. Use a visible <label> with for unless you have a very specific reason not to.

2. Error Messages Need ARIA Attributes

When validation fails, sighted users see a red error message. Screen reader users need to hear it. Use aria-describedby to link error messages to their inputs, and role="alert" to announce them dynamically.

<label for="password">Password</label>
<input
  id="password"
  name="password"
  type="password"
  aria-invalid={!!$errors.password}
  aria-describedby={$errors.password ? 'password-error' : undefined}
/>
{#if $errors.password}
  <p id="password-error" role="alert" class="text-red-600">
    {$errors.password}
  </p>
{/if}

What Each Attribute Does

  • aria-invalid="true" — tells screen readers this field has an error
  • aria-describedby="password-error" — links the input to its error message, so when the user focuses the input, the error is read aloud
  • role="alert" — announces the error immediately when it appears, even if the user hasn’t focused the field

3. Use Fieldsets for Related Groups

Radio groups and checkbox groups should be wrapped in <fieldset> with a <legend>. This tells screen readers that the options belong together.

<fieldset>
  <legend>Preferred contact method</legend>
  <label><input type="radio" name="contact" value="email" /> Email</label>
  <label><input type="radio" name="contact" value="phone" /> Phone</label>
  <label><input type="radio" name="contact" value="sms" /> SMS</label>
</fieldset>

Without the fieldset, a screen reader reads “Email, radio button” without context. With it, the reader says “Preferred contact method: Email, radio button, 1 of 3.”

4. Keyboard Navigation Must Work

Every form element must be reachable and operable via keyboard. Test by pressing Tab through the entire form:

  • Can you reach every field with Tab?
  • Is the tab order logical (top to bottom, left to right)?
  • Can you select radio/checkbox options with Arrow keys?
  • Can you submit with Enter?
  • Can you close modals/dropdowns with Escape?

Custom select components and date pickers are the most common keyboard traps. If you’re using shadcn-svelte components (which SvelteForms uses), these are already keyboard-accessible. If you’re building custom components, follow WAI-ARIA patterns.

5. Focus Management After Submission

After a form submits successfully, where does focus go? If the form disappears and is replaced by a success message, focus should move to that message. If the form stays visible with errors, focus should move to the first error field.

const { form, errors, enhance } = superForm(data.form, {
  validators: zod(schema),
  onUpdated: ({ form }) => {
    if (!form.valid) {
      // Focus the first field with an error
      const firstError = document.querySelector('[aria-invalid="true"]');
      if (firstError instanceof HTMLElement) firstError.focus();
    }
  }
});

6. Required Fields: Be Explicit

Mark required fields visually (asterisk, “required” label) AND programmatically:

<label for="name">
  Name <span aria-hidden="true">*</span>
  <span class="sr-only">(required)</span>
</label>
<input id="name" name="name" required aria-required="true" />

The visual asterisk is hidden from screen readers with aria-hidden="true", and a screen-reader-only “(required)” text is added instead, so screen readers say “Name, required” rather than “Name, asterisk.”

7. Color Is Not Enough

Red error text is a visual cue, but it’s insufficient for colorblind users. Add an icon, a prefix (“Error:”), or a border change in addition to color:

<p class="text-red-600 flex items-center gap-1">
  <svg class="w-4 h-4" ...><!-- error icon --></svg>
  {$errors.email}
</p>

8. Test with Real Assistive Technology

Automated tools (axe, Lighthouse) catch about 30% of accessibility issues. The rest require manual testing:

  • VoiceOver (macOS) — built-in, free, Cmd+F5 to toggle
  • NVDA (Windows) — free download, the most-used screen reader
  • Keyboard only — unplug your mouse and navigate with Tab/Enter/Arrows

Test every form interaction: field navigation, error announcements, submission feedback, and any modals or dropdowns.

Accessibility in Generated Code

The code generated by SvelteForms includes proper label-input association with for/id attributes, error messages with conditional display, and disabled submit buttons during loading. For additional ARIA attributes (like aria-invalid and aria-describedby), add them to the generated component after export — they’re field-specific and benefit from human judgment about which fields need extra annotation.

Read more: 10 production form best practices, validation patterns, integration guide.

Build forms faster

Try the form builder →