Rotating Announcement Carousel Above the Header, With Pause Control
An auto-rotating promo bar rendered by the layout above the header: a looped Swiper of up to ten text blocks that reserves its height before any script runs, waits for the carousel library on a bounded retry, and gives the shopper a real pause control whose label always says what it will do next.
A layout-level announcement bar renders before the theme's carousel library has loaded, so it cannot assume Swiper exists. This section reserves its own height, ships the first message as plain server HTML, polls for the library on a hard budget of thirty attempts, and only then rotates — with a pause/play toggle that rewrites its own accessible name to match the action it will perform.
The Problem
An announcement bar is one line of text. A promo calendar is not. On any given week a beauty storefront is carrying a shipping threshold, an active sale and a loyalty push at the same time, and a single static bar can show one of them. The two usual answers are both bad: stack three bars and give away the viewport above the fold, or rotate them with a script and accept that the bar appears, then resizes, then starts moving, while the header underneath it jumps.
WCAG adds a third requirement that most rotating bars miss. Content that moves on its own for more than five seconds needs a way for the visitor to stop it — Success Criterion 2.2.2 — and hovering to pause doesn't count, because a keyboard or screen-reader user never hovers. A carousel with no pause control is an accessibility defect at the very top of every page.
The Constraint
The bar lives in theme.liquid, above the header. That position decides everything about how it has to be built. It renders before nearly all of the theme's JavaScript, and in particular before the vendor bundle that provides Swiper — so the section cannot call new Swiper() on load and hope. It also cannot wait indefinitely: a script that polls forever for a library that never arrives is a leak on every page of the store.
Because it sits above the header, it also cannot change size when it initializes. Anything that shifts the header pushes the whole page down with it, on every template, on every visit.
And because it rotates, it needs a control. Not hover-to-pause, which only helps a pointer user, but an operable button with an accessible name that stays true as the state changes.
What We Built
announcement-carousel.liquid is a section rendered by the layout inside a .section-above-header wrapper. Its markup is a Swiper track of text blocks — at most ten — with previous and next buttons and a pause/play toggle, all of which render only when there is more than one block.
Reserved space. The wrapper declares a min-height of 44px, raised to 54px on small screens through a .section-above-header override, so the bar occupies its final height in the first paint and the header position doesn't move when the carousel takes over.
The first message is server HTML. The first block is in the initial document as ordinary text. If nothing else happens — no library, no script — that message is still visible. Everything after this point is enhancement.
A section-scoped controller. An immediately-invoked function resolves its own root by [data-announcement-carousel][data-section-id='<section.id>'], so two placements of the section, or a theme-editor re-render, each find their own element. It reads the slide count and stops immediately when there is only one. It then checks swiperEl.swiper and returns if a Swiper instance already exists, which makes a second run harmless.
Bounded polling. If window.Swiper isn't defined yet, the controller retries every 100ms, up to thirty times. When the library arrives it initializes with loop: true, a 600ms transition, a 4000ms autoplay delay, disableOnInteraction: false and pauseOnMouseEnter: true. If it never arrives, the loop gives up silently and slide one stays exactly where the server put it.
The toggle. The pause/play button starts and stops autoplay, swaps its two icon spans, and rewrites its own aria-label on every change — "Pause carousel" while rotating, "Play carousel" while stopped — so the name a screen reader announces is always the action the next press will perform.
Why This Way
The order of checks is the design. Bailing on a single slide first means the most common configuration — one announcement — costs no JavaScript at all, no library lookup, no timer. Checking for an existing instance second is what makes the section safe to render twice or to re-render in the theme editor; Swiper initialized twice on one element is a broken slider. Polling last, and only up to a budget, is the honest answer to being rendered before the vendor bundle: the section can't control when the library arrives, so it waits in a way that is guaranteed to end.
The bar's height is declared in CSS rather than measured in JavaScript because the layout needs it before any script runs. A measured height would arrive after the header had already been positioned.
What we gave up is flexibility in the polling. Thirty attempts at 100ms is three seconds, and a shopper on a connection slow enough to blow that budget gets a static bar with the first announcement and no rotation. We took that over an unbounded wait, because a static bar is a correct page and an orphaned timer is not.
Why Not an App
Announcement-bar apps are a mature category, and for a store that wants scheduling, geotargeting or countdowns without touching the theme, one is a defensible purchase. The usual mechanism is a script that injects the bar after the page has loaded and lays it out then.
This is the one place on the page where that shape hurts most. Injected above the header, an app's bar arrives after the header has been positioned and pushes everything down when it appears. The section here is in the document from the first byte with its height already reserved, and the enhancement runs on top of markup that is already correct. It also keeps the pause control in the theme, where its aria-label behavior can be read, tested and kept.
Implementation Notes
- The reserved height is
min-height: 44pxon the wrapper and 54px under the.section-above-headeroverride for small screens, so the bar's box is settled before the carousel exists. - Retry budget is explicit: thirty attempts at 100ms, after which the controller stops silently. Nothing is logged and nothing is left running.
- Previous and next buttons and the pause toggle only render when there is more than one block; a single announcement emits none of the chrome.
- Swiper's navigation elements are passed as node references rather than selector strings, so nothing in the initialization depends on an id being unique across the document.
- The preset ships two example slides, so a merchandiser adding the section in the theme editor sees a rotating carousel immediately instead of a bar that looks broken with one block.
- Autoplay uses
disableOnInteraction: false, so pressing an arrow moves the slide without permanently killing rotation, andpauseOnMouseEnter: true, so a shopper reading a message isn't yanked off it.
Edge Cases
- A single announcement short-circuits before Swiper is touched. The common case costs no JavaScript beyond reading a count.
- If Swiper never loads, the first slide still renders as plain server-side HTML. The bar is static, not empty.
- The toggle's
aria-labelis rewritten on each state change rather than set once, so a screen reader never announces "Pause carousel" on a bar that is already paused. - Hover pauses rotation and arrow clicks do not disable it, so a shopper can read a slide and keep the bar working afterwards.
- Long promo copy wraps rather than truncating —
white-space: normalandword-break: break-word— so a message is never clipped to fit the track. - A second run of the controller on an element that already has a Swiper instance returns immediately instead of stacking a second slider on the first.
Platform Primitives Used
liquid-schema-settings— the section schema defines the block type, the ten-block ceiling and a preset with two example slides; the controller reads nothing that isn't in that schema.theme-blocks— each announcement is a block, so a merchandiser can reorder or remove messages without a developer, and the slide count is a count of blocks.
Integrations in Play
- Swiper — the carousel library the section waits for. It arrives with the theme's vendor bundle, after this section has rendered, which is the reason the bounded retry exists.
Where It Runs
Globally: the section is rendered by theme.liquid above the header on every page, rather than being placed in a template. That is why it carries no template list — there is no page on which it isn't present — and why its size had to be settled before any script runs.
What This Demonstrates
- Accessibility remediation at component level — the primary capability: an operable pause control on auto-rotating content, with an accessible name that tracks state rather than a label set once and left stale.
- Announcement bars and promotional messaging — the merchandising job the section does: several concurrent messages in the space of one bar.
How We Know
One section file from the theme, roughly 360 lines of Liquid, markup, styling and controller together, plus three client working records: two task-register entries and a phase specification. The task register logs moving the Canadian storefront's announcement bar to the carousel format already used on the main store, with iterative mobile fixes; the specification calls for a pause/play button whenever more than one announcement rotates. That condition — more than one — is the same one the controller tests before it renders the toggle or touches the library. The requirement is in the client's record, and the code shows how it was met; that is what strong confidence means on this page.
Related Builds
- Accessible play/pause web component for autoplaying hero video — the same storefront, the same WCAG criterion, applied to video instead of text: a control whose state is driven by the thing it controls.
- Audience-targeted announcement bar with sticky-height measurement — a different storefront's announcement bar, built inside Dawn's own section: the cross-client comparison, where the bar publishes its height as a custom property instead of reserving it in CSS.
- Accessible link-label helpers for repeated CTAs — this storefront's other accessible-name work, where the name comes from the link target rather than from state.
- Homepage hero carousel v2 - video and split-image slides with per-slide typography control — the other Swiper on the same storefront, rendered in a template rather than the layout, where the library is already present when the section initializes.
The Buy-vs-Build Question
An app buys scheduling and targeting and pays for it with a bar that arrives after the page. Building it bought a bar that's in the first byte, a control whose accessible name is honest, and a retry that ends — and cost the scheduling features an app would have included. Whether that trade holds for your promo calendar is the question on the accessibility remediation decision page.
Provenance & Evidence
- Client: Nudestix — nudestix.com
- Surface: Global — rendered by the layout above the header
- Templates served: none listed; the section is mounted by
theme.liquid, not by a template - Complexity: Medium
- Attribution: Deploi-authored. The section, its controller and the pause control are ours. It runs in a theme built on Dawn 6.0.2 and rotates on Swiper, which the theme's vendor bundle supplies.
- Status: Live, verified 2026-09-07
- Evidence: One section file from the theme, plus two task-register entries and a phase specification from client working sessions
- Confidence: Strong — the client record names the requirement; the code shows how it was met
- Primary capability: Accessibility remediation at component level
Ready for a Promo Bar Every Shopper Can Stop?
You dream it. We build it. If your announcement bar rotates and nothing on the page can pause it, Contact us today and we'll build the control into the bar — before the library loads, not after.