Tutorials
Building Accessible Web Components with Livewire
Accessibility is not optional. Here is how we build inclusive Livewire components that work for everyone.
4 min readVeetek team
Travel booking flows are long and detailed. Customers choose dates, enter passenger names exactly as they appear in passports, select seats, add bags and pay. If any step cannot be completed with a keyboard or a screen reader, a customer cannot book. For many travel businesses, accessibility is also a legal requirement in the markets they serve.
We build much of the Vee Travel interface with Livewire. This article explains the practices we follow to keep Livewire components accessible, with examples from booking forms.
Start with semantic HTML
Most accessibility problems come from replacing native elements with custom ones. A div with a click handler is not a button: it cannot receive keyboard focus and screen readers do not announce it as interactive. Livewire works well with plain HTML, so use the right element for the job.
Use button for actions and a for navigation.
Use real form controls with label elements linked by for and id.
Use fieldset and legend to group related inputs, such as each passenger's details.
Use headings in order so screen reader users can move through the page structure.
A passenger form is a good example. Each passenger becomes a fieldset with a legend such as "Passenger 2, child", so a screen reader user always knows which traveller they are editing.
Announce dynamic changes
Livewire updates parts of the page without a full reload. Sighted users see the change. Screen reader users may not, unless the update is announced. Use live regions for messages that appear after an action.
<div aria-live="polite" class="sr-only">
@if ($priceChanged)
The total price has changed to {{ $formattedTotal }}.
@endif
</div>
Use polite for most updates, such as a recalculated price or a filter result count. Reserve assertive for urgent messages, such as a session about to expire. Keep announcements short and avoid announcing every small change.
Validation that everyone can follow
Livewire's real-time validation is convenient, but errors must be connected to their fields. When an error appears, link it with aria-describedby and mark the field as invalid.
<label for="pax-{{ $i }}-surname">Surname</label>
<input id="pax-{{ $i }}-surname"
wire:model.blur="passengers.{{ $i }}.surname"
aria-invalid="{{ $errors->has("passengers.$i.surname") ? 'true' : 'false' }}"
aria-describedby="pax-{{ $i }}-surname-error">
@error("passengers.$i.surname")
<p id="pax-{{ $i }}-surname-error">{{ $message }}</p>
@enderror
Validate on blur rather than on every keystroke, so users are not interrupted while typing. When a form is submitted with errors, move focus to a summary at the top that lists each error as a link to its field.
Manage focus and keyboard interaction
When Livewire replaces content, focus can be lost and return to the top of the page. This is disorienting for keyboard and screen reader users. Handle focus explicitly in a few common cases:
After moving to the next booking step, send focus to the new step's heading.
When opening a modal, such as a fare rules dialog, move focus into it, keep it there while open and return it to the trigger when closed.
After removing an item, such as a passenger or a room, move focus to a sensible nearby element.
Livewire lets you dispatch a browser event from the component and handle focus with a few lines of Alpine.js on the front end.
Some components have no native equivalent: date range pickers, seat maps and airport autocompletes. These need ARIA roles and keyboard support that match established patterns. An autocomplete should follow the combobox pattern, with arrow keys to move between suggestions and Enter to select. A seat map should let users move between seats with arrow keys and announce each seat's row, letter, price and availability.
Where a custom widget is complex, offer a simpler alternative. A seat map can include a list view of available seats, which is easier for many users and not only those using assistive technology.
Test as part of normal work
Accessibility is easiest when checked continuously. We run automated checks with axe in our browser tests to catch missing labels and contrast problems, and we test key flows with only a keyboard and with a screen reader before release. Automated tools find a portion of issues. Manual testing finds the rest.
Accessible booking flows are clearer booking flows. Proper labels, visible focus, understandable errors and predictable updates help every customer, including the agent booking quickly from a keyboard and the traveller using a phone in bright sunlight. Livewire does not get in the way of any of this. It simply requires the same care as any other way of building interfaces.