Skip to main content

Notes

Gift Card and Promo Code Fields: The Error States Most Checkout Forms Get Wrong

Quietramp ·

Gift Card and Promo Code Fields: The Error States Most Checkout Forms Get Wrong

A gift card or promo code field is one of the smallest inputs on a checkout page — one text box, one “Apply” button — but it behaves differently from almost everything else on the form. A name or address field just collects text; a code field validates what the shopper typed against a live, external answer (is this code real, unused, unexpired, applicable to this order), and it usually has to handle more than one applied code at once. That validation step and that multi-item state are exactly where the accessibility gaps in this component tend to live.

Quick answer: the failures cluster around what happens after a shopper submits a code, not the input itself. An invalid or expired code needs to be described in text and wired to the field it belongs to (SC 3.3.1 Error Identification), a successfully applied code needs its own accessible name if it renders as a removable list item (SC 4.1.2 Name, Role, Value), and the order total needs to announce its new value every time a code is added or removed, not just once (SC 4.1.3 Status Messages).

This article is general accessibility information, not legal advice. Meeting WCAG 2.1 AA does not resolve ADA or any other legal compliance question — consult qualified counsel for legal risk questions.

The label, briefly

The input itself needs the same fix every text field needs: a real, programmatically associated label, not a placeholder standing in for one. WebAIM’s guide to advanced form labeling is direct about why placeholder text alone doesn’t hold up: the placeholder attribute “does not take the place of a <label>” and “disappears once information has been entered into the field, making it more difficult for users to make corrections.” That’s SC 3.3.2 Labels or Instructions (Level A) for the requirement itself and SC 1.3.1 Info and Relationships (Level A) for the for/id association that makes it programmatically real, not just visually present. This part isn’t unique to gift card fields — see our back-in-stock forms guide for the same fix applied to a different field. What’s actually distinct about a redemption code field is everything downstream of submission.

<label for="promo-code">Gift card or promo code</label>
<input type="text" id="promo-code" name="promo-code" autocomplete="off">
<button type="submit">Apply</button>

An invalid code needs to be described in text, tied to the field

Submit a real name and it’s almost always accepted. Submit a gift card or promo code and there’s a real chance it’s wrong — mistyped, expired, already redeemed, or not valid for this order. That failure path gets exercised constantly, and it’s the part most implementations handle worst: a red border, or a small “Invalid code” message that appears somewhere near the button with no connection to the field itself.

The Understanding doc for SC 3.3.1 Error Identification (Level A) states the requirement plainly: “If an input error is automatically detected, the item that is in error is identified and the error is described to the user in text.” It also specifies what “identified” means in practice — the description has to name the actual problem: “‘Email is not valid’ would pass 3.3.1, but ‘Please provide a valid email address in the format [email protected]’ also conveys how it can be fixed and passes both.” Applied here, that means distinguishing “This code has expired” from “This code has already been used” from “This code doesn’t apply to items in your cart” — not one generic “Invalid code” for every failure reason, since a shopper can’t act on a message that doesn’t say what’s actually wrong.

Two mechanisms make that description reach a screen reader user, and they solve two different problems. Technique ARIA21 ties the error to the field itself: set aria-invalid="true" on the input and point aria-describedby at the element holding the error text, so a screen reader announces the field as invalid and reads the reason the moment it receives focus. Technique ARIA19 gets the message announced the instant it appears, independent of where focus is: “The aria-live attribute makes it possible for an AT (such as a screen reader) to be notified when error messages are injected into a Live Region container.” Two details from that technique are easy to get wrong in a way that fails silently — the error container has to already exist in the DOM on page load (not be created only once an error happens), since “the error container must be present in the DOM on page load for the error message to be spoken by most screen readers,” and it needs aria-atomic="true", which the technique notes “is necessary to make Voiceover on iOS read the error messages after more than one invalid submission.”

<label for="promo-code">Gift card or promo code</label>
<input
  type="text"
  id="promo-code"
  name="promo-code"
  aria-invalid="true"
  aria-describedby="promo-error"
>
<button type="submit">Apply</button>
<div id="promo-error" role="alert" aria-atomic="true">
  This code has expired.
</div>

role="alert" is equivalent to aria-live="assertive" — appropriate here, since a failed redemption is the kind of interruption a shopper needs immediately, not on a delay.

A stack of applied codes is a dynamic list, not a single confirmation

Many stores let a shopper apply more than one code — a gift card plus a percentage-off promo, say — and each successfully applied code typically renders as its own line item with an icon-only “×” or trash-can button to remove it. That turns this component into a small dynamic list, which brings back SC 4.1.2 Name, Role, Value (Level A) in a slightly different shape than a single trigger button: an icon-only remove control needs an accessible name specific to the item it removes, not a generic one. “Remove” announced identically for three different applied codes gives a screen reader user no way to tell which one they’re about to delete.

<!-- Fails: three identical, unidentifiable remove buttons -->
<button aria-label="Remove"><svg aria-hidden="true">…</svg></button>

<!-- Passes -->
<button aria-label="Remove gift card ending in 4521">
  <svg aria-hidden="true" focusable="false">…</svg>
</button>

The total has to announce its new value every time, not once

Adding or removing a code changes the order total, and that change needs to reach a screen reader user the same way the visual update reaches a sighted one. SC 4.1.3 Status Messages (Level AA) covers this directly, and its own guidance on how to phrase the update matters as much as whether one fires at all: “If a status message persists on the page, modifications to this text are usually equivalent to a new status message… where only the number in this string was coded as an updated chunk of content, the resulting experience for screen reader users could be to only hear ‘three’, which may not be sufficient information to provide context for the user.” The fix the doc points to is aria-atomic on the containing element, so the entire updated string is announced as one unit — “New total: $42.50” — rather than just the changed digits in isolation. Use role="status" (per Technique ARIA22) rather than role="alert" here: a total updating is routine feedback, not an error, so it should queue politely instead of interrupting.

What a scanner catches here, and what needs a person

axe-core’s label rule (tagged wcag412 in its own rule descriptions) reliably flags a redemption input with no associated label at all. What it can’t evaluate: whether submitting an invalid code actually triggers a spoken announcement, whether that announcement names the real reason for the failure, or whether removing one gift card out of three correctly identifies which one was removed. There is no axe-core rule for status-message behavior, because confirming it requires triggering the interaction and listening to what a screen reader actually says — not something a static scan of the DOM can determine on its own. That gap is exactly why this component needs a manual pass, not just a clean automated report.

FAQ

Should an invalid-code error use role="alert" or role="status"? role="alert" (or aria-live="assertive") for the error itself — it needs to interrupt, since the shopper is blocked until it’s resolved. role="status" (or aria-live="polite") for the resulting total update once a code is successfully applied or removed — that’s routine feedback, not an interruption.

Does every applied code need its own live-region announcement when it’s added? Yes, for the same reason the total does — a code added without any announcement leaves a screen reader user unsure whether the “Apply” click did anything at all. It doesn’t need a separate announcement from the total update if you can combine them into one clear status message, such as “Gift card applied. New total: $42.50.”

Is a percentage-off promo code any different from a fixed-dollar gift card for these requirements? No — both are the same redemption-field pattern from an accessibility standpoint. The WCAG requirements described here apply to the field, its error state, and its effect on the total regardless of which kind of code it is.

Get your checkout forms checked by a person, not just a parser

A clean automated scan on a gift-card or promo-code field only confirms the label exists — it says nothing about whether an error is actually announced or whether a shopper can tell which applied code they’re removing. Quietramp’s $890 audits include a manual pass through interactive checkout components like this one, verified by a person operating the flow with a keyboard and screen reader. See a real sample report or check pricing — $99/month for ongoing re-checks after fixes ship.


This is educational content about technical accessibility standards, not legal advice. Meeting WCAG 2.1 AA does not guarantee legal compliance with the ADA or any other law, and nothing here should be read as a compliance guarantee. Consult qualified counsel for legal risk questions.

This article was drafted with AI assistance and reviewed by a person for accuracy before publication.

← Back to Notes