Controlling Screen Reader Only Content Dynamically With Alpine
AI generated
x-data
Alpine
Alpine.js · Accessibility
Controlling Screen Reader Only Content Dynamically With Alpine
How an icon button with no visible text still communicates its current state clearly to screen reader users

A heart icon for saving a product, a sort button with an arrow symbol, a close cross in the top corner of a modal: for sighted users, icon buttons like these are usually self-explanatory, precisely because they stay compact with no distracting accompanying text. For screen reader users, the state of such a button often stays unclear, especially once it changes dynamically, for example between save and remove. This article covers how sr-only classes combined with x-text and x-show deliver extra context audible exclusively to screen readers, without touching the carefully designed visual interface at all.

9 min read sr-only x-text visually hidden

1. What sr-only classes are and how they work technically

An sr-only class, already available in the Hyvä theme through Tailwind, positions an element absolutely outside the visible viewport, shrinks its width and height down to a single pixel, and clips any overflowing content, without ever using display:none or visibility:hidden. That distinction is the key part: an element stays fully reachable in the accessibility tree, and therefore for screen readers, even though it is visually invisible.

Technically speaking, an sr-only class is not an accessibility feature in the narrow sense, but a pure CSS trick that deliberately decouples an element's visual layer from its semantic layer. A sighted user never notices this text at all, while a screen reader user gets it read out exactly like any other visible text on the page.


/* Tailwind sr-only utility, already available in the Hyva theme */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

2. The decisive difference: display:none versus sr-only

An element with display:none gets fully removed from the render tree and therefore no longer exists in the accessibility tree either, making it just as invisible to a screen reader as to sighted users. If such an element inside an Alpine component gets controlled with x-show instead of x-if, it stays in the DOM, but at false it gets removed from the accessibility tree in exactly the same way, through an inline display:none.

sr-only, by contrast, stays fully present in the accessibility tree, because neither display nor visibility gets changed, only the element's position and size. Anyone trying to toggle an extra screen reader text in and out with x-show therefore unintentionally produces the exact opposite of what sr-only is meant to achieve: a text that becomes unreachable for everyone the moment it is meant to be visually hidden.

3. Combining x-text with sr-only for extra context

The real value shows up once an sr-only element gets bound with x-text to the very same Alpine state that also drives an icon's or button's visible presentation, so the extra screen reader text changes automatically in sync with the visual state, without needing to be maintained in two separate places in the code. A single reactive state then drives icon, color, and the extra screen reader text at once.

This combination fits especially well for cases where an icon alone is unambiguous for sighted users, but its current state would not be recognizable for screen reader users without extra text, for example whether a filled heart icon means a product is already saved or is available to be saved. The sr-only text makes that distinction explicit while the icon stays visually unchanged.

4. x-show and x-if for conditional screen reader hints

Beyond a plain text swap through x-text, x-show can also toggle an entire sr-only block conditionally, for example an extra warning that should only appear as screen reader text when a specific condition applies, such as stock falling below a critical threshold. What matters here is that x-show works correctly in this case, precisely because the element is meant to be invisible in the false state anyway, screen readers included.

For a block that should never exist in the DOM at all while the condition does not hold, for example because it contains expensive computation, x-if combined with a template tag fits better than x-show, since it genuinely avoids creating DOM nodes rather than merely leaving them invisible. For the vast majority of simple sr-only text blocks, though, x-text with a conditional string is entirely enough.

5. Practical example: an icon button with a state description

A save button with a heart icon shows this pattern concretely: the SVG icon itself carries aria-hidden="true", since it is purely decorative and should not carry its own semantic meaning, while an sr-only span inside the same button describes the current state as a full sentence through x-text. The button itself needs no additional aria-label because its text content, even though visually hidden, already provides the complete accessible description.

What matters is that the sr-only text changes in sync with the visual icon state on every click, so a screen reader user learns immediately after every interaction whether the action succeeded and what the next click would do, exactly as a sighted user can read off the filled or unfilled heart icon.


<button
  x-data="{ saved: false }"
  @click="saved = !saved"
  class="p-2 rounded hover:bg-gray-100"
>
  <svg
    class="w-6 h-6"
    :class="saved ? 'fill-red-600' : 'fill-none stroke-gray-600'"
    aria-hidden="true"
  >
    <!-- heart path -->
  </svg>
  <span class="sr-only" x-text="saved ? 'Remove from wishlist' : 'Add to wishlist'"></span>
</button>

6. Practical example: extra context for sort and filter buttons

Another typical example is a sort button in a product listing header, whose arrow icon visually flips between ascending and descending. Without extra text, a screen reader user hearing just Price Sort has no way to know the current sort direction or what another click would do. An sr-only span that explicitly states the current sort direction and what the next click will do closes that gap.

For a filter button with an active count, for example a badge showing the number of currently applied filters, that count should also get embedded inside sr-only text, such as 3 filters active, open filters, instead of relying on a screen reader correctly inferring the context of an isolated number next to an icon.

7. aria-live versus sr-only: which tool for which purpose

sr-only text bound with x-text describes an element's current state permanently, and a screen reader only reads it out once the user actively navigates to or focuses that element, for example via the Tab key. An aria-live region, by contrast, gets announced proactively as soon as its content changes, regardless of where focus currently sits, as covered in the separate live regions article in this series.

For an icon button state like the save example, sr-only with x-text is the right choice, because the information only becomes relevant once the user reaches the button again, not immediately after every click regardless of focus. An additional aria-live announcement here would be redundant and, with frequent use, would irritate more than it helps, which is why both mechanisms should be used deliberately for different purposes.

8. Performance and reflow considerations for frequently changing sr-only text

Since an sr-only element gets taken out of normal document flow through position: absolute, a text change via x-text usually triggers no visible reflow in the surrounding layout, which makes this approach unproblematic even for very frequently changing states, for example a live counter caption that updates on every keystroke.

Even so, an sr-only text should not get updated on every single intermediate change of a value if that same value is also bound to a live region, since that produces the same overload described in the live regions article of this series. For a plain state text with no aria-live, only read out on focus, a high change frequency is unproblematic, since it does not get announced proactively.

9. Common mistake: sr-only text hides important information from sighted users

A subtle but frequent mistake is putting a piece of information exclusively inside sr-only text, even though it would actually matter equally to every user, for example an error message or an important warning that got hidden rather than shown for visual design reasons. sr-only should never serve as a way to hide information from sighted users, only to provide extra context that gets conveyed visually some other way already, for example through an icon or a color.

The reliable test for any new sr-only text is: would a sighted user need the same information too, to understand the current state. If so, it belongs visibly in the markup, not inside an sr-only block. Only when the information is already unambiguously recognizable for sighted users some other way, for example through an icon's fill state, is sr-only the right place for the extra, purely textual confirmation of that state.

Technique Visible to sighted users Reachable for screen readers Typical use
sr-only class No Yes, permanently Extra context for icon buttons
display:none / x-show=false No No Content irrelevant to everyone
aria-hidden="true" Yes No Purely decorative icons with no own meaning
aria-live region Usually also hidden with sr-only Yes, announced proactively Status messages independent of focus
x-if with template No, node not in the DOM at all No, while the condition is false Expensive or rare conditional blocks

Mironsoft

Alpine.js interactivity for Hyvä frontends

A Hyvä frontend that needs more interactivity, but without React overhead?

We build interactive frontend components for Hyvä themes with Alpine.js, lightweight and without build-step complexity, from simple toggles to complex form flows.

Custom Components

Develop interactive Alpine.js components for specific shop requirements.

Performance Review

Review existing Alpine.js implementations for reactivity pitfalls and performance.

Team Training

Bring developers up to speed on Alpine.js patterns for Hyvä themes hands-on.

10. Summary

Screen Reader Only Content With Alpine: Key Takeaways

Core principle

sr-only decouples visual presentation from screen reader reachability, without using display:none.

x-text binding

A single reactive Alpine state drives both the icon and the sr-only text at once, with no duplicated maintenance.

Versus aria-live

sr-only only gets read out on focus, aria-live gets announced proactively on every change.

Key rule

sr-only provides extra context, but never hides information that sighted users need too.

11. FAQ: Screen Reader Only Content With Alpine: Key Takeaways

1How does an sr-only class work technically?
It positions an element absolutely outside the viewport and shrinks its size down to one pixel, without using display:none or visibility:hidden. The element stays fully reachable in the accessibility tree and for screen readers.
2What is the difference between sr-only and display:none?
display:none removes an element from both the render tree and the accessibility tree, making it invisible to screen readers too. sr-only stays present in the accessibility tree because only position and size get changed, not visibility itself.
3Why is x-show unsuitable for a permanent sr-only text?
x-show sets an inline display:none at false, which removes the element from the accessibility tree too. For text that should always stay reachable for screen readers, x-text on a permanently present element gets used instead.
4How does x-text combine with sr-only to provide context?
An sr-only element gets bound to the same Alpine state that also drives an icon's visible presentation, so the screen reader text changes automatically in sync with the visual state.
5Does an icon button with sr-only text also need an aria-label?
No, an sr-only text inside the button already provides the complete accessible description. An additional aria-label would be redundant and would need to be kept in sync separately.
6When should aria-hidden="true" be set on an icon?
When the icon is purely decorative and should not carry its own semantic meaning, for example because an accompanying sr-only text already provides the relevant information in full.
7What is the difference between sr-only with x-text and an aria-live region?
sr-only with x-text only gets read out once the user actively navigates to or focuses that element. An aria-live region gets announced proactively as soon as its content changes, regardless of current focus.
8Does frequently changing sr-only text cause performance problems?
Generally not, since position: absolute takes the element out of normal document flow and a text change triggers no visible reflow. It only becomes a problem if the same text is also bound to an aria-live region.
9What is the most common mistake when using sr-only text?
A piece of information gets placed exclusively inside sr-only text even though it would matter to sighted users too. sr-only should never hide information from sighted users, only provide extra context.
10How can you check whether a piece of information really belongs in an sr-only block?
The test is: would a sighted user need the same information too, to understand the state. If so, it belongs visibly in the markup, not only inside an sr-only block.