Country/Market Selector Hardened for Three Simultaneous Instances
Dawn's country selector, rendered three times on every page, with every option id namespaced by the position that rendered it and three ARIA attributes removed — so the three instances stop colliding and the vendor's own localization script needed no change at all.
Dawn's country selector gives each option an id derived from the country name, which is unique exactly once. A storefront that renders the selector in the announcement bar, the desktop header and the mobile drawer gets every id three times, so labels and lookups resolve to the wrong instance. We rebuilt the ids from a render-position prefix and the ISO code, and removed three ARIA roles that were wrong or redundant, without touching Dawn's JavaScript.
The Problem
An HTML id is unique per document. Dawn's country-localization.liquid builds each country option's id from country.name, which is fine when the snippet renders once. On this storefront it renders three times on every page — in the announcement bar's utility row, in the desktop header and in the mobile drawer — because the shopper needs to reach the market switch from wherever they are.
Three copies means every id appears three times. A <label for> points at the first match, wherever it is. Any getElementById lookup, including the ones assistive technology performs to resolve aria-labelledby and aria-controls, lands on the wrong instance two times out of three. And a country name is a localized string with spaces and punctuation, so even the one correct id is awkward to target and changes with the shop's language.
Two of the vendor's ARIA attributes made it worse. The country filter input carried role="combobox", which it is not — it's a text filter over a list — so a screen reader announced a control the input couldn't behave as. And two <ul> elements carried role="list", which an unstyled list already has.
The Constraint
The snippet is shared by all three call sites, so the fix couldn't live in any one of them. It had to come from the caller: each render passes a localPosition argument, and that argument is the only thing distinguishing one instance from another. Whatever made ids unique had to be built from it.
Dawn's localization-form.js supplies the country search behavior — the filter, the popular-countries shortcut, the keyboard handling — and it works from the markup's structure. Restructuring the form to avoid the collision would have meant either forking that script or losing what it does. The markup had to keep the shape the script expects.
And the fix had to be small enough to diff against Dawn 15.4.1, so a future theme update could be reconciled against a known set of changes rather than a rewritten file.
What We Built
country-localization.liquid is Dawn's snippet with two kinds of change and no others.
Ids rebuilt from position and ISO code. Every option id is now {{ localPosition }}-country-{{ country.iso_code | downcase }}, and every entry in the popular-countries list is {{ localPosition }}-popular-country-{{ ... }}, instead of an id derived from country.name. The three call sites pass distinct positions: AnnouncementCountry from the announcement bar, HeaderCountry and HeaderCountryMobile from the header's desktop and mobile renders. Three instances, three prefixes, no shared id anywhere in the document.
Three attributes removed. role="combobox" is gone from the filter input, so it is announced as what it is. role="list" is gone from the two <ul> elements that already had that role by default. Nothing was added to compensate; the correction here is a deletion.
localization-form.js is byte-identical to Dawn 15.4.1. The script never depended on the id scheme — it navigates the form by structure — so changing the ids changed nothing it reads. That is the point of fixing this in markup.
Why This Way
ISO codes are stable, unique and safe inside an id. Country names are none of those: they're localized, they contain spaces and punctuation, and they change when the shop's language does. Prefixing by render position is the minimum change that makes every id globally unique, because position is the one thing each call site already knows about itself and the snippet doesn't.
Removing roles rather than correcting them is the smaller fix and the right one. A combobox role on an input that filters a list promises a pattern — a popup, aria-expanded, aria-activedescendant — that the vendor's markup doesn't deliver, and adding the rest of that pattern would have meant rebuilding the control. Removing the claim leaves an input that behaves exactly as it says it does.
There is a maintenance obligation in this, and it lives in a vendor file. Two deltas against Dawn's snippet have to be carried through every theme update, and nothing warns anyone when an update overwrites them. We kept the diff to exactly those two kinds of change so that the reconciliation is a few lines to re-apply, not a file to re-understand.
Implementation Notes
- Verified by diff against Dawn 15.4.1: the id scheme and the three ARIA removals are the only deltas in the file.
localization-form.jsin the same cluster is byte-identical to Dawn 15.4.1. No local JavaScript change was needed, which is what fixing this in markup buys.- Rendered once from
announcement-bar.liquidand twice fromheader.liquid, with the threelocalPositionvaluesAnnouncementCountry,HeaderCountryandHeaderCountryMobile. - Country codes are downcased in the id, so ids are consistent regardless of the casing Shopify returns for
iso_code. - The popular-countries list gets its own
-popular-country-prefix, so a country appearing in both lists has two distinct ids within one instance as well as across instances.
Edge Cases
- The popular-countries list repeats countries that also appear in the full list, so without the second prefix an instance would collide with itself; each list has its own.
aria-currenton the active country is preserved from the vendor's markup, so the selected market is still announced as current.- Switching the announcement-bar selector off leaves the header's two instances with ids that were never dependent on the third, so nothing renumbers.
- A shop with a single market suppresses the selector entirely at the call site; the snippet never renders with one option.
Platform Primitives Used
shopify-markets— the country list, theiso_codeon each country and the localization form the snippet wraps are what Markets provides; the fix is in how the theme labels them.
Where It Runs
Globally. The snippet is rendered from the announcement bar section and from the header section's desktop and mobile paths, all three of which live in the storefront's header group and appear on every template. There is no template list because there is no template on which the selector is absent.
What This Demonstrates
- Accessibility remediation at component level — the primary capability: ids that survive being rendered three times, and roles removed where they claimed a pattern the control didn't implement.
- Geo routing and market switching — the switch itself is Markets; this build is what keeps it operable when it's on the page three times.
How We Know
One Dawn snippet diffed against Dawn 15.4.1, plus the vendor script it works with confirmed unchanged, plus two client scoping documents. The documents establish that the rebuilt theme serves two storefronts — loveurcurls.ca and loveurcurls.com — with consistent design and functionality. A market selector therefore has to work everywhere, and on this theme that means rendering three times. The id scheme and the ARIA deletions are not described in them; they are code-side decisions, read from the diff. The client's record explains why the selector matters. The diff explains what it needed.
Related Builds
- Audience-targeted announcement bar with sticky-height measurement — the utility bar that renders the first of the three instances.
- Adults/Kids audience-switching header with automatic menu swap — the header that renders the other two, in its desktop and drawer paths.
- Arched-label icon grid PDP block — the same storefront, the same class of defect in a component we authored: an SVG path id composed from
block.idso a repeated block never collides with itself. - Dawn media gallery hardened for hidden slides and deferred video — the same approach to Dawn code: the smallest diff that fixes the behavior, verified against the vendor version.
The Buy-vs-Build Question
An accessibility overlay works on the page after the theme has rendered it, and a duplicated id is a defect in what the theme renders. This one was fixed where the markup is written, in two kinds of change against a vendor snippet, with the vendor's script untouched. Where component-level fixes belong in the theme and where a tool earns its place is the question on the accessibility remediation decision page.
Provenance & Evidence
- Client: LUS Brands — loveurcurls.com
- Surface: Global — the storefront's header group
- Templates served: none listed; the snippet is rendered by two header-group sections on every template
- Complexity: Low
- Attribution: Built on top of Dawn 15.4.1; this snippet is our modification.
country-localization.liquidis Dawn's, with an id scheme and three ARIA removals as the only changes, verified by diff.localization-form.jsis Dawn's, unchanged. - Status: Live, verified 2026-09-07
- Evidence: One Dawn snippet diffed against 15.4.1 and one vendor script confirmed identical, plus two client scoping documents
- Confidence: Strong — the client record establishes the two-storefront requirement; the diff shows exactly what changed
- Primary capability: Accessibility remediation at component level
Ready to Fix What Breaks When a Component Renders Twice?
You dream it. We build it. If a vendor snippet is correct once and wrong everywhere else it appears, Contact us today and we'll find the smallest change that makes every instance its own.