Controlling accessibility directly in the markup
With the aria-* modifiers in Tailwind CSS, ARIA attributes become the single source of truth for accessibility states, visually and semantically at once. No separate class management, no JavaScript for visual states, no drifting apart of ARIA state and CSS class.
Table of Contents
- 1. Why ARIA utilities are a paradigm shift for accessibility
- 2. Syntax of the aria-* modifiers in Tailwind CSS
- 3. aria-expanded: controlling accordion and dropdown states
- 4. aria-selected: tabs, listboxes and selection states
- 5. aria-disabled: communicating disabled elements correctly
- 6. aria-checked: custom checkboxes and toggle buttons
- 7. data-* modifiers as a complement to ARIA
- 8. group-aria and peer-aria: propagating parent-child states
- 9. Comparison: ARIA modifiers vs. JavaScript class management
- 10. Summary
- 11. FAQ
1. Why ARIA utilities are a paradigm shift for accessibility
The classic problem when implementing accessible UI components: the ARIA state and the visual presentation are managed separately. An accordion element needs aria-expanded="true" for screen readers and a separate CSS class like .is-open for the visual presentation. In JavaScript you have to keep both in sync, setting the ARIA attribute and adding the class. Miss one of the two steps and the visual state and the semantic state drift apart. This is exactly the problem Tailwind CSS ARIA utilities solve: the ARIA attribute itself becomes the CSS selector.
The Tailwind CSS ARIA utilities were introduced in v3.2 and use CSS attribute selectors like [aria-expanded="true"] under the hood. The class aria-expanded:block produces [aria-expanded="true"] { display: block; }. That means if you set aria-expanded="true" via JavaScript, as Alpine.js does, the visual state is automatically rendered correctly, without managing a separate CSS class. ARIA is now the single source of truth for the component state.
2. Syntax of the aria-* modifiers in Tailwind CSS
The Tailwind CSS ARIA utilities follow the pattern aria-{attribute}:{utility}. Here {attribute} stands for an ARIA attribute in boolean form, such as aria-expanded, aria-selected, aria-disabled, aria-checked, aria-hidden. The generated CSS selector is [aria-{attribute}="true"]. Tailwind CSS knows the most common boolean ARIA attributes out of the box and generates selectors for them automatically.
For ARIA attributes with non-boolean values, such as aria-sort="ascending" or aria-current="page", Tailwind offers bracket syntax: aria-[sort=ascending]:bg-sky-50 produces [aria-sort="ascending"] { background-color: ... }. This extension makes the Tailwind CSS ARIA utilities usable for more complex ARIA scenarios too, where the attribute value is not simply true or false but represents a state from a defined set of values.
<!-- aria-expanded: Accordion toggle, ARIA attribute drives visual state -->
<button
aria-expanded="false"
@click="$el.setAttribute('aria-expanded', $el.getAttribute('aria-expanded') === 'true' ? 'false' : 'true')"
class="flex items-center justify-between w-full px-5 py-4 font-semibold text-slate-800
border border-slate-200 rounded-xl hover:bg-slate-50 transition-colors
aria-expanded:bg-sky-50 aria-expanded:border-sky-200 aria-expanded:text-sky-900"
>
How do ARIA utilities work in Tailwind?
<!-- Arrow rotates when expanded -->
<svg class="w-5 h-5 transition-transform duration-200 aria-expanded:rotate-180"
aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<!-- aria-current: navigation active state -->
<nav>
<a href="/produkte" aria-current="page"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors
text-slate-600 hover:text-slate-900 hover:bg-slate-100
aria-[current=page]:bg-sky-600 aria-[current=page]:text-white">
Products
</a>
<a href="/blog"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors
text-slate-600 hover:text-slate-900 hover:bg-slate-100
aria-[current=page]:bg-sky-600 aria-[current=page]:text-white">
Blog
</a>
</nav>
3. aria-expanded: controlling accordion and dropdown states
aria-expanded is the most widely used boolean ARIA attribute in interactive components. It tells screen readers whether an associated panel is visible or hidden, for accordions, dropdowns, menus and disclosure widgets. With Tailwind CSS ARIA utilities and Alpine.js an elegant pattern emerges: Alpine sets aria-expanded via :aria-expanded="open.toString()", and Tailwind automatically drives all visual states from it, background color, text color, rotation of the chevron icon.
The companion panel, which is shown when aria-expanded="true", should ideally not use a separate show/hide flag but instead be controlled via the button's ARIA attribute and a CSS selector. The pattern peer-aria-expanded:block makes it possible to reveal a sibling element as soon as the button is marked as "expanded", without a single line of JavaScript for visibility. ARIA and CSS talk directly to each other.
4. aria-selected: tabs, listboxes and selection states
aria-selected communicates the selection state in tab bars, listboxes and tree widgets. With Tailwind CSS ARIA utilities and the aria-selected: modifier, you can style the active tab directly from the ARIA attribute: aria-selected:bg-white aria-selected:shadow-sm aria-selected:text-sky-700. The ARIA attribute that must be set for assistive technology simultaneously serves as the CSS selector, no .tab.is-active class pattern is needed.
Important with tab components: aria-selected="false" must be set explicitly on all non-active tabs, not just on the active one. This is a common accessibility gap. Tailwind CSS ARIA utilities help maintain this discipline because the visual presentation depends directly on the ARIA attribute: if aria-selected is not set correctly on all elements, the interface also visually looks wrong. ARIA correctness thereby indirectly enforces visual correctness as well.
<!-- Tab list using aria-selected for both accessibility and visual state -->
<div x-data="{ active: 'overview' }">
<div role="tablist" class="flex gap-1 bg-slate-100 p-1 rounded-xl">
<!-- Tab buttons: aria-selected drives all visual state -->
<button
role="tab"
:aria-selected="(active === 'overview').toString()"
@click="active = 'overview'"
class="flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-150
text-slate-600
aria-selected:bg-white aria-selected:shadow-sm aria-selected:text-sky-700"
>Overview</button>
<button
role="tab"
:aria-selected="(active === 'specs').toString()"
@click="active = 'specs'"
class="flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-150
text-slate-600
aria-selected:bg-white aria-selected:shadow-sm aria-selected:text-sky-700"
>Specifications</button>
<button
role="tab"
:aria-selected="(active === 'reviews').toString()"
@click="active = 'reviews'"
class="flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-150
text-slate-600
aria-selected:bg-white aria-selected:shadow-sm aria-selected:text-sky-700"
>Reviews</button>
</div>
<!-- Tab panels, shown by Alpine, but aria-selected is the source of truth -->
<div role="tabpanel" x-show="active === 'overview'" class="mt-4 p-4">
<p class="text-slate-700">Overview content goes here.</p>
</div>
<div role="tabpanel" x-show="active === 'specs'" class="mt-4 p-4">
<p class="text-slate-700">Specification content goes here.</p>
</div>
</div>
5. aria-disabled: communicating disabled elements correctly
The difference between disabled (the HTML attribute) and aria-disabled="true" matters in practice. The native disabled attribute on a button removes the element from the tab order and prevents all events, screen readers can no longer focus it and therefore cannot announce it either. aria-disabled="true", on the other hand, marks the element as disabled but leaves it in the tab order and reachable for assistive technology. This is the correct choice when you want to explain to the user why the element is disabled, for example via a tooltip.
With Tailwind CSS ARIA utilities, aria-disabled="true" can be used directly for the visual presentation: aria-disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:pointer-events-none. The visual appearance is identical to a natively disabled button, but the element remains focusable and reachable for screen readers. In forms, wizards and multi-step processes, this pattern is more valuable than the native disabled attribute.
6. aria-checked: custom checkboxes and toggle buttons
Native HTML checkboxes can be styled with the :checked CSS pseudo-class, but custom checkboxes built for design reasons need a different strategy. With role="checkbox" and aria-checked you build a fully accessible custom checkbox that communicates its state correctly to screen readers. Tailwind CSS ARIA utilities and the aria-checked: modifier then drive the visual presentation of the custom element directly from the ARIA attribute.
The same pattern applies to toggle buttons: role="switch" with aria-checked="true/false" is the semantically correct implementation of an on/off switch. Tailwind's aria-checked:bg-sky-600 on the switch track and aria-checked:translate-x-5 on the switch thumb produce the full toggle effect, purely from ARIA state, without separate JavaScript class manipulation for the visuals.
<!-- Custom toggle switch: aria-checked drives all visual state -->
<button
role="switch"
:aria-checked="enabled.toString()"
@click="enabled = !enabled"
x-data="{ enabled: false }"
class="relative inline-flex h-6 w-11 items-center rounded-full border-2
border-transparent transition-colors duration-200 focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-2
bg-slate-200 aria-checked:bg-sky-600"
>
<span class="sr-only">Enable email notifications</span>
<!-- Thumb: slides right when aria-checked=true -->
<span class="pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow
ring-0 transition-transform duration-200
translate-x-0 aria-checked:translate-x-5">
</span>
</button>
<!-- Custom checkbox group with aria-checked -->
<div role="group" aria-labelledby="options-label">
<p id="options-label" class="font-semibold text-slate-800 mb-3">Notifications</p>
<div class="space-y-2">
<div
role="checkbox"
tabindex="0"
x-data="{ checked: false }"
:aria-checked="checked.toString()"
@click="checked = !checked"
@keydown.space.prevent="checked = !checked"
class="flex items-center gap-3 cursor-pointer group"
>
<!-- Visual checkbox box: border and background change on aria-checked -->
<span class="w-5 h-5 rounded border-2 border-slate-300 flex items-center justify-center
transition-colors aria-checked:border-sky-600 aria-checked:bg-sky-600 group-aria-checked:border-sky-600 group-aria-checked:bg-sky-600">
<svg class="w-3 h-3 text-white opacity-0 group-aria-checked:opacity-100 transition-opacity"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
</svg>
</span>
<span class="text-slate-700">New orders</span>
</div>
</div>
</div>
7. data-* modifiers as a complement to ARIA
Alongside Tailwind CSS ARIA utilities, Tailwind has also offered data-* modifiers since v3.2, which work similarly. data-[active]:bg-sky-50 applies when the element has data-active or data-active="true" set. These modifiers are suited to states that carry no semantic ARIA meaning, UI states like "currently open", "active during drag" or "marked as favorite". The clear separation in practice: ARIA attributes for states that must be communicated to assistive technology; data-* attributes for purely visual or application-internal states.
In Alpine.js, data-* attributes can be set simply with :data-active="isActive" or x-bind:data-active="isActive". Tailwind automatically picks this up with data-[active]:.... This combination of Alpine.js state management and Tailwind CSS ARIA utilities plus data-* modifiers produces a complete, JavaScript-light system for controlling UI states, without a single manually managed CSS class for state.
8. group-aria and peer-aria: propagating parent-child states
Tailwind CSS lets you propagate the ARIA state of a parent element to its child elements via the group mechanism. A parent element gets the class group, and child elements use group-aria-expanded:rotate-180 to react to the parent's ARIA state. This is especially valuable for accordion headers: the button container is the group element, and the icon or description text inside the same container can react to the button's aria-expanded attribute.
The peer-aria-* pattern works analogously for sibling elements: an element with the class peer and aria-expanded lets the following sibling element become visible with peer-aria-expanded:block. This is the CSS-native approach to showing and hiding panels, without any JavaScript visibility management, based purely on ARIA attributes and Tailwind CSS ARIA utilities. These patterns, together with Alpine.js for setting the ARIA attribute, add up to a complete, accessible and maintainable component architecture.
9. Comparison: ARIA modifiers vs. JavaScript class management
The concrete advantage of Tailwind CSS ARIA utilities over classic JavaScript class management shows most clearly in a code comparison. Both approaches solve the same problem, but with different implications for maintainability, accessibility correctness and bug proneness.
| Criterion | JS class management | Tailwind ARIA utilities | Advantage |
|---|---|---|---|
| Source of truth | ARIA + CSS class (2x) | ARIA attribute (1x) | No sync bug possible |
| JavaScript effort | Change ARIA + classList | Only set ARIA | 50% less JS code |
| WCAG correctness | Ensured manually | Structurally enforced | Less ARIA is forgotten |
| Debugging | Check ARIA + class | Only check ARIA | Simpler diagnosis |
| Non-boolean ARIA | Standard | Bracket syntax needed | JS classes more flexible |
The Tailwind CSS ARIA utilities clearly win with boolean attributes like aria-expanded, aria-selected, aria-disabled and aria-checked. For more complex attribute values, such as aria-sort, aria-level, the bracket syntax remains capable, but somewhat less readable than an explicit CSS class. The rule of thumb: for all boolean ARIA attributes, always use the Tailwind ARIA utilities; for non-boolean attributes, use bracket syntax or explicit CSS classes depending on readability preference.
Mironsoft
Tailwind CSS, Hyva Themes and accessible frontend development
Need accessible Tailwind components for your project?
We implement ARIA-compliant UI components with Tailwind CSS ARIA utilities and Alpine.js, from tab bars to custom forms to full WCAG 2.1 AA compliance.
Accessibility audit
Analysis of existing components for ARIA correctness and WCAG compliance
Component development
Tabs, accordions, modals and custom forms with ARIA utilities and Alpine.js
WCAG compliance
Full WCAG 2.1 AA compliance to demonstrate legal requirements are met
10. Summary
The Tailwind CSS ARIA utilities make ARIA attributes the single source of truth for accessibility states, visually and semantically in one. The aria-expanded: modifier eliminates the classic synchronization problem between ARIA attribute and CSS class entirely. aria-selected: for tab components, aria-disabled: for accessibly disabled elements, aria-checked: for custom checkboxes and toggle buttons, all these patterns become clearer, more maintainable and less error-prone through Tailwind ARIA utilities.
The group-aria-* and peer-aria-* patterns extend these possibilities to parent-child and sibling communication. Complemented by data-* modifiers for purely visual states with no ARIA semantics, a complete, JavaScript-light system for state control in UI components emerges. Together with Alpine.js as the state manager, which only sets the ARIA attribute, and Tailwind as the visual system, a clean separation results: JavaScript for logic, ARIA for semantics, Tailwind CSS ARIA utilities for visuals.
Tailwind CSS ARIA Utilities - The Essentials at a Glance
Boolean modifiers
aria-expanded:, aria-selected:, aria-disabled:, aria-checked: - usable directly for all boolean ARIA attributes. No brackets needed.
Bracket syntax
aria-[current=page]:, aria-[sort=ascending]: - for ARIA attributes with specific string values. Fully flexible.
group and peer patterns
group-aria-expanded:rotate-180 on child elements, peer-aria-expanded:block on sibling elements - state propagation without JavaScript.
data-* as a complement
data-[active]: for purely visual states with no ARIA semantics. ARIA for screen reader states, data-* for app states.
11. FAQ: Tailwind CSS ARIA Utilities
1From which Tailwind version are ARIA utilities available?
2aria-disabled vs. native disabled?
3Setting aria-expanded in Alpine.js?
:aria-expanded="open.toString()" - Alpine binds the boolean state as a string. Conversion is necessary because ARIA attributes are string values.