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 erroraria-describedby="password-error"— links the input to its error message, so when the user focuses the input, the error is read aloudrole="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 →