del/ins semantics for old prices, text alternative discount badges, and understandable price ranges
A struck through old price next to a highlighted new price is one of the strongest visual signals in e-commerce, but plain CSS strikethrough via text-decoration stays completely invisible to screen reader users. Using the semantic elements del and ins correctly instead, giving discount badges a text alternative, and phrasing price ranges for product variants clearly turns a purely visual price treatment into information every user can reliably retrieve.
Table of Contents
- 1. Why plain CSS strikethrough is not enough for old prices
- 2. The correct HTML elements: del for the old price, ins for the new price
- 3. How screen readers actually handle del and ins in practice
- 4. Practical pattern: visible price plus hidden context text
- 5. Marking up percentage discount badges with a text alternative
- 6. Announcing price ranges for product variants clearly and dynamically
- 7. Color coding for discounts and contrast requirements
- 8. Currency and number format readability for screen readers
- 9. Implementation in Magento and Hyvä: adapting the price block template
- 10. Summary
- 11. FAQ
1. Why plain CSS strikethrough is not enough for old prices
An old price is usually struck through visually via text-decoration: line-through on an arbitrary span element. This CSS property is a pure style declaration with no semantic meaning at all, it changes neither the role nor the accessible name of the element in the accessibility tree. A screen reader therefore reads out the struck through price exactly the same way as the new price, with no indication whatsoever that it represents a now invalid, earlier value.
That loses exactly the information that makes a reduced price recognizable as a discount in the first place: the comparison between an old and a new value. For sighted users this comparison is immediately obvious through the strikethrough line, for screen reader users without extra markup all that remains is a confusing sequence of two price figures with no recognizable relationship between them.
2. The correct HTML elements: del for the old price, ins for the new price
HTML provides exactly the semantic elements meant for this use case with del and ins: del marks content removed from the document or made invalid, ins marks newly inserted content. A reduced price can be marked up accordingly with del for the original price and ins for the current, reduced price, which semantically captures exactly the relationship between the two values.
Many browsers already render del with a strikethrough line by default and ins underlined by default, so using these elements can even save additional CSS for the pure visualization. It still matters to explicitly define the styling yourself instead of relying on browser defaults, since those can vary by browser and by user stylesheet.
<p class="price">
<del class="price__old">79.99 EUR</del>
<ins class="price__new">59.99 EUR</ins>
</p>
<style>
.price__old { text-decoration: line-through; color: #71717a; }
.price__new { text-decoration: none; font-weight: 600; color: #18181b; }
</style>
3. How screen readers actually handle del and ins in practice
In practice, not every screen reader automatically announces the semantic meaning of del and ins as audible extra information, some simply read the content as plain text without mentioning "struck through" or "inserted". The correct elements are still the right foundation, since they at least carry the correct semantic role in the accessibility tree and get used correctly by assistive technologies that do support this announcement.
For reliable results across every screen reader, an additional aria-label or a visible, summarizing text is recommended, one that captures the entire price comparison in a single sentence, regardless of whether the individual assistive technology reads out del and ins or not. This redundant safeguard is the same principle already used to secure reliable announcements for the star rating and the wishlist toggle.
4. Practical pattern: visible price plus hidden context text
The most robust pattern combines del and ins with an additional, visually hidden sentence that summarizes the entire price comparison in natural language: "Instead of 79.99 euros, now 59.99 euros, you save 25 percent." This sentence is placed as an sr-only element directly next to or inside the price container and delivers a complete, understandable announcement regardless of how a given screen reader handles del/ins.
This pattern matters especially for product listings with many reduced items, since a user quickly scanning through the list would otherwise hear two isolated numbers with no context multiple times, instead of a clear, self contained statement for every item.
<p class="price">
<span class="sr-only">Instead of 79.99 euros, now 59.99 euros, you save 25 percent.</span>
<del class="price__old" aria-hidden="true">79.99 EUR</del>
<ins class="price__new" aria-hidden="true">59.99 EUR</ins>
</p>
5. Marking up percentage discount badges with a text alternative
A discount badge often shows only a short figure like "-25%" in a colored circle or rectangle at the top left of the product image. Without accompanying text it stays unclear to a screen reader what this figure refers to, it could theoretically also mean a rating, a stock quantity, or something else. An aria-label like "25 percent discount" directly on the badge element resolves this ambiguity unambiguously.
If the badge is purely decorative and the actual discount information already exists in full as text elsewhere, for example in the price comparison from the previous section, the badge can instead be removed from the accessibility tree with aria-hidden="true" to avoid duplicate announcements. Which of the two techniques fits depends on whether the badge is the sole source of the discount information or merely a visual repetition.
<span class="discount-badge" aria-label="25 percent discount">-25%</span>
6. Announcing price ranges for product variants clearly and dynamically
Configurable products with multiple variants often display a price range like "from 19.99 euros" as long as no specific variant has been selected yet. This text should always include the word "from" or an equivalent phrasing, rather than displaying only the lowest figure in isolation, otherwise it creates the impression of a fixed price instead of a range.
Once the user selects a specific variant, for example a particular size or color, the displayed price changes dynamically from the range to a fixed value. This change must be announced via an aria-live="polite" region, since otherwise the price gets updated silently in an already rendered, not newly focused element, and screen reader users simply miss the new, binding figure.
<p id="product-price" aria-live="polite">
from 19.99 EUR
</p>
<script>
function updatePriceForVariant(price) {
document.getElementById('product-price').textContent = price + ' EUR';
}
</script>
7. Color coding for discounts and contrast requirements
A reduced price is frequently also shown in red visually to set it apart from regular prices. Under WCAG success criterion 1.4.1, color must never be the only distinguishing feature, which is why the strikethrough line on the old price, the changed font weight on the new price, or an additional badge must always be preserved as a non color signal, even when red is additionally used.
The contrast of the red price text against the background must meet the same WCAG minimum as any other text, at least 4.5 to 1 for normal body text. A common mistake is a too bright, eye catching red that looks striking but fails the required contrast ratio on a light background, making it hard to read for users with visual impairments.
8. Currency and number format readability for screen readers
Large price figures with thousands separators, for example "1,299.00 EUR", get read out differently by some screen readers depending on language and locale settings, in rare cases even as a sequence of individual digits instead of one coherent number. A correctly set lang attribute on the HTML document or the respective price element makes sure the screen reader's speech engine applies the right pronunciation rule for number formats.
Currency symbols like "EUR" or "€" should be used consistently, either always as an abbreviation or always as a symbol, since some screen readers correctly pronounce the euro symbol as "euro" while others skip it silently or announce it as an unknown character. A textual abbreviation like "EUR" is in practice the more reliable choice for consistent announcements across different assistive technologies.
9. Implementation in Magento and Hyvä: adapting the price block template
In a Hyvä theme, the del/ins pattern can be added directly to the price render template of the product listing and product detail page, so the original Magento price block still serves as the central data source but gets output with correct semantic markup and an additional sr-only context sentence. The dynamic price update on variant selection runs through the same Alpine.js component that also controls the variant selector.
The inline script block for the aria-live price update must be registered via $hyvaCsp->registerInlineScript() in the corresponding phtml template, so the Content Security Policy does not block execution.
document.addEventListener('alpine:init', () => {
Alpine.data('variantPrice', () => ({
priceLabel: 'from 19.99 EUR',
updatePrice(newPrice) {
this.priceLabel = newPrice + ' EUR';
},
}));
});
| Price element | Technique | Purpose | Common mistake |
|---|---|---|---|
| Old price | del element instead of text-decoration | Marking semantically as invalid | Plain CSS strikethrough on a span |
| New price | ins element | Marking semantically as the current value | No semantic element, only bold text |
| Full price comparison | sr-only context sentence | Complete statement regardless of screen reader | Only two isolated numbers with no connection |
| Discount badge | aria-label with the percentage spelled out | Clarifying what the figure refers to | A plain number with no context in the badge |
| Price range for variants | aria-live region on change | Announcing the new price after selection | Silent update with no live region |
Mironsoft
WCAG audits, accessible Magento shops, and training
Not sure whether the shop is actually accessible?
We audit existing Magento shops against WCAG 2.2, fix concrete barriers in the Hyvä frontend, and train teams so accessibility stays anchored in the development process for good.
WCAG Audit
Systematically review the shop against WCAG 2.2 AA, with a prioritized issue list.
Fixing Barriers
Concrete implementation: keyboard operability, screen reader support, contrast, forms.
Team Training
Raise developer and editor awareness for accessible implementation day to day.
10. Summary
Accessible Price Display: The Essentials
Core idea
del and ins replace plain CSS strikethrough and represent the relationship between old and new price semantically correctly.
Redundant safeguard
An sr-only context sentence with the full price comparison secures the announcement regardless of how a screen reader handles del/ins.
Discount badges
An aria-label with the percentage spelled out clarifies the meaning of an isolated figure inside the badge.
Price ranges
A live region announces the final price after variant selection, since otherwise the value changes silently in the DOM.