Flash Sale Announcement Bar — Fast, High-Contrast, Dismissible
The urgency pattern e-commerce lives on: a fast, high-contrast marquee bar pinned to the top of the page shouting a limited-time offer, with a working promo-code link, a dismiss button that collapses the bar with pure CSS (:has() + a checkbox), and a grid-template-rows animation so the page content slides up smoothly instead of jumping.
Published
The code
<section class="mqs-06" aria-label="Flash sale announcement bar demo">
<input class="mqs-06__closeBox" type="checkbox" id="mqs-06-close">
<div class="mqs-06__collapse">
<div class="mqs-06__clip">
<div class="mqs-06__bar">
<div class="mqs-06__viewport">
<div class="mqs-06__group">
<span class="mqs-06__msg">Flash sale — 40% off everything ends tonight</span><span class="mqs-06__zap" aria-hidden="true">⚡</span>
<span class="mqs-06__msg">Use code <a class="mqs-06__code" href="#deal">BLITZ40</a> at checkout</span><span class="mqs-06__zap" aria-hidden="true">⚡</span>
</div>
<div class="mqs-06__group" aria-hidden="true">
<span class="mqs-06__msg">Flash sale — 40% off everything ends tonight</span><span class="mqs-06__zap">⚡</span>
<span class="mqs-06__msg">Use code <span class="mqs-06__code">BLITZ40</span> at checkout</span><span class="mqs-06__zap">⚡</span>
</div>
</div>
<label class="mqs-06__closeBtn" for="mqs-06-close" title="Dismiss announcement"><span aria-hidden="true">✕</span><span class="mqs-06__sr">Dismiss announcement</span></label>
</div>
</div>
</div>
<div class="mqs-06__page">
<p class="mqs-06__hint">↑ Page content — dismiss the bar and watch it glide up (pure CSS, no JS).</p>
</div>
</section><section class="mqs-06" aria-label="Flash sale announcement bar demo">
<input class="mqs-06__closeBox" type="checkbox" id="mqs-06-close">
<div class="mqs-06__collapse">
<div class="mqs-06__clip">
<div class="mqs-06__bar">
<div class="mqs-06__viewport">
<div class="mqs-06__group">
<span class="mqs-06__msg">Flash sale — 40% off everything ends tonight</span><span class="mqs-06__zap" aria-hidden="true">⚡</span>
<span class="mqs-06__msg">Use code <a class="mqs-06__code" href="#deal">BLITZ40</a> at checkout</span><span class="mqs-06__zap" aria-hidden="true">⚡</span>
</div>
<div class="mqs-06__group" aria-hidden="true">
<span class="mqs-06__msg">Flash sale — 40% off everything ends tonight</span><span class="mqs-06__zap">⚡</span>
<span class="mqs-06__msg">Use code <span class="mqs-06__code">BLITZ40</span> at checkout</span><span class="mqs-06__zap">⚡</span>
</div>
</div>
<label class="mqs-06__closeBtn" for="mqs-06-close" title="Dismiss announcement"><span aria-hidden="true">✕</span><span class="mqs-06__sr">Dismiss announcement</span></label>
</div>
</div>
</div>
<div class="mqs-06__page">
<p class="mqs-06__hint">↑ Page content — dismiss the bar and watch it glide up (pure CSS, no JS).</p>
</div>
</section>.mqs-06,
.mqs-06 *,
.mqs-06 *::before,
.mqs-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.mqs-06 {
min-height: 100vh;
width: 100%;
--gap: 36px;
--speed: 14s;
font-family: 'Segoe UI',system-ui,sans-serif;
background: #fafaf9;
}
.mqs-06__closeBox {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.mqs-06__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.mqs-06__collapse {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows .3s ease;
}
.mqs-06:has(.mqs-06__closeBox:checked) .mqs-06__collapse {
grid-template-rows: 0fr;
}
.mqs-06__clip {
min-height: 0;
overflow: hidden;
}
.mqs-06__bar {
display: flex;
align-items: stretch;
background: #fde047;
border-bottom: 2px solid #1c1917;
}
.mqs-06__viewport {
flex: 1;
display: flex;
gap: var(--gap);
overflow: hidden;
padding-block: 11px;
}
.mqs-06__group {
flex: none;
display: flex;
align-items: center;
gap: var(--gap);
animation: mqs-06-scroll var(--speed) linear infinite;
will-change: transform;
}
@keyframes mqs-06-scroll {
to {
transform: translateX(calc(-100% - var(--gap)));
}
}
.mqs-06__viewport:hover .mqs-06__group,
.mqs-06__viewport:focus-within .mqs-06__group {
animation-play-state: paused;
}
.mqs-06__msg {
flex: none;
font-size: 13px;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
color: #1c1917;
}
.mqs-06__zap {
flex: none;
font-size: 12px;
}
.mqs-06__code {
color: #1c1917;
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-thickness: 2px;
border-radius: 3px;
}
.mqs-06__code:hover {
background: #1c1917;
color: #fde047;
text-decoration: none;
padding-inline: 4px;
}
.mqs-06__code:focus-visible {
outline: 2px solid #1c1917;
outline-offset: 2px;
}
.mqs-06__closeBtn {
flex: none;
display: grid;
place-items: center;
width: 42px;
font-size: 14px;
font-weight: 700;
color: #1c1917;
cursor: pointer;
border-left: 2px solid #1c1917;
transition: background .15s;
}
.mqs-06__closeBtn:hover {
background: #facc15;
}
.mqs-06__closeBox:focus-visible ~ .mqs-06__collapse .mqs-06__closeBtn {
outline: 2px solid #1c1917;
outline-offset: -4px;
}
.mqs-06__page {
padding: 56px 24px 64px;
text-align: center;
}
.mqs-06__hint {
font-size: 13px;
color: #a8a29e;
}
@media (prefers-reduced-motion: reduce) {
.mqs-06__group {
animation: none;
}
.mqs-06__viewport {
overflow-x: auto;
}
.mqs-06__collapse {
transition: none;
}
}.mqs-06,
.mqs-06 *,
.mqs-06 *::before,
.mqs-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.mqs-06 {
min-height: 100vh;
width: 100%;
--gap: 36px;
--speed: 14s;
font-family: 'Segoe UI',system-ui,sans-serif;
background: #fafaf9;
}
.mqs-06__closeBox {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.mqs-06__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.mqs-06__collapse {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows .3s ease;
}
.mqs-06:has(.mqs-06__closeBox:checked) .mqs-06__collapse {
grid-template-rows: 0fr;
}
.mqs-06__clip {
min-height: 0;
overflow: hidden;
}
.mqs-06__bar {
display: flex;
align-items: stretch;
background: #fde047;
border-bottom: 2px solid #1c1917;
}
.mqs-06__viewport {
flex: 1;
display: flex;
gap: var(--gap);
overflow: hidden;
padding-block: 11px;
}
.mqs-06__group {
flex: none;
display: flex;
align-items: center;
gap: var(--gap);
animation: mqs-06-scroll var(--speed) linear infinite;
will-change: transform;
}
@keyframes mqs-06-scroll {
to {
transform: translateX(calc(-100% - var(--gap)));
}
}
.mqs-06__viewport:hover .mqs-06__group,
.mqs-06__viewport:focus-within .mqs-06__group {
animation-play-state: paused;
}
.mqs-06__msg {
flex: none;
font-size: 13px;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
color: #1c1917;
}
.mqs-06__zap {
flex: none;
font-size: 12px;
}
.mqs-06__code {
color: #1c1917;
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-thickness: 2px;
border-radius: 3px;
}
.mqs-06__code:hover {
background: #1c1917;
color: #fde047;
text-decoration: none;
padding-inline: 4px;
}
.mqs-06__code:focus-visible {
outline: 2px solid #1c1917;
outline-offset: 2px;
}
.mqs-06__closeBtn {
flex: none;
display: grid;
place-items: center;
width: 42px;
font-size: 14px;
font-weight: 700;
color: #1c1917;
cursor: pointer;
border-left: 2px solid #1c1917;
transition: background .15s;
}
.mqs-06__closeBtn:hover {
background: #facc15;
}
.mqs-06__closeBox:focus-visible ~ .mqs-06__collapse .mqs-06__closeBtn {
outline: 2px solid #1c1917;
outline-offset: -4px;
}
.mqs-06__page {
padding: 56px 24px 64px;
text-align: center;
}
.mqs-06__hint {
font-size: 13px;
color: #a8a29e;
}
@media (prefers-reduced-motion: reduce) {
.mqs-06__group {
animation: none;
}
.mqs-06__viewport {
overflow-x: auto;
}
.mqs-06__collapse {
transition: none;
}
}/* No JavaScript — dismissal is a checkbox + :has(:checked) flipping grid-template-rows 1fr→0fr (the modern animate-to-height-auto trick). Add 2 lines of localStorage if dismissal should persist. */
/* No JavaScript — dismissal is a checkbox + :has(:checked) flipping grid-template-rows 1fr→0fr (the modern animate-to-height-auto trick). Add 2 lines of localStorage if dismissal should persist. */Here's a working CSS Infinite Marquee Design from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Flash Sale Announcement Bar — Fast, High-Contrast, Dismissible
Source: https://codefronts.com/motion/css-infinite-marquee/flash-sale-announcement-bar/
The urgency pattern e-commerce lives on: a fast, high-contrast marquee bar pinned to the top of the page shouting a limited-time offer, with a working promo-code link, a dismiss button that collapses the bar with pure CSS (:has() + a checkbox), and a grid-template-rows animation so the page content slides up smoothly instead of jumping.
## HTML
```html
<section class="mqs-06" aria-label="Flash sale announcement bar demo">
<input class="mqs-06__closeBox" type="checkbox" id="mqs-06-close">
<div class="mqs-06__collapse">
<div class="mqs-06__clip">
<div class="mqs-06__bar">
<div class="mqs-06__viewport">
<div class="mqs-06__group">
<span class="mqs-06__msg">Flash sale — 40% off everything ends tonight</span><span class="mqs-06__zap" aria-hidden="true">⚡</span>
<span class="mqs-06__msg">Use code <a class="mqs-06__code" href="#deal">BLITZ40</a> at checkout</span><span class="mqs-06__zap" aria-hidden="true">⚡</span>
</div>
<div class="mqs-06__group" aria-hidden="true">
<span class="mqs-06__msg">Flash sale — 40% off everything ends tonight</span><span class="mqs-06__zap">⚡</span>
<span class="mqs-06__msg">Use code <span class="mqs-06__code">BLITZ40</span> at checkout</span><span class="mqs-06__zap">⚡</span>
</div>
</div>
<label class="mqs-06__closeBtn" for="mqs-06-close" title="Dismiss announcement"><span aria-hidden="true">✕</span><span class="mqs-06__sr">Dismiss announcement</span></label>
</div>
</div>
</div>
<div class="mqs-06__page">
<p class="mqs-06__hint">↑ Page content — dismiss the bar and watch it glide up (pure CSS, no JS).</p>
</div>
</section>
```
## CSS
```css
.mqs-06,
.mqs-06 *,
.mqs-06 *::before,
.mqs-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.mqs-06 {
min-height: 100vh;
width: 100%;
--gap: 36px;
--speed: 14s;
font-family: 'Segoe UI',system-ui,sans-serif;
background: #fafaf9;
}
.mqs-06__closeBox {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.mqs-06__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.mqs-06__collapse {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows .3s ease;
}
.mqs-06:has(.mqs-06__closeBox:checked) .mqs-06__collapse {
grid-template-rows: 0fr;
}
.mqs-06__clip {
min-height: 0;
overflow: hidden;
}
.mqs-06__bar {
display: flex;
align-items: stretch;
background: #fde047;
border-bottom: 2px solid #1c1917;
}
.mqs-06__viewport {
flex: 1;
display: flex;
gap: var(--gap);
overflow: hidden;
padding-block: 11px;
}
.mqs-06__group {
flex: none;
display: flex;
align-items: center;
gap: var(--gap);
animation: mqs-06-scroll var(--speed) linear infinite;
will-change: transform;
}
@keyframes mqs-06-scroll {
to {
transform: translateX(calc(-100% - var(--gap)));
}
}
.mqs-06__viewport:hover .mqs-06__group,
.mqs-06__viewport:focus-within .mqs-06__group {
animation-play-state: paused;
}
.mqs-06__msg {
flex: none;
font-size: 13px;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
color: #1c1917;
}
.mqs-06__zap {
flex: none;
font-size: 12px;
}
.mqs-06__code {
color: #1c1917;
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-thickness: 2px;
border-radius: 3px;
}
.mqs-06__code:hover {
background: #1c1917;
color: #fde047;
text-decoration: none;
padding-inline: 4px;
}
.mqs-06__code:focus-visible {
outline: 2px solid #1c1917;
outline-offset: 2px;
}
.mqs-06__closeBtn {
flex: none;
display: grid;
place-items: center;
width: 42px;
font-size: 14px;
font-weight: 700;
color: #1c1917;
cursor: pointer;
border-left: 2px solid #1c1917;
transition: background .15s;
}
.mqs-06__closeBtn:hover {
background: #facc15;
}
.mqs-06__closeBox:focus-visible ~ .mqs-06__collapse .mqs-06__closeBtn {
outline: 2px solid #1c1917;
outline-offset: -4px;
}
.mqs-06__page {
padding: 56px 24px 64px;
text-align: center;
}
.mqs-06__hint {
font-size: 13px;
color: #a8a29e;
}
@media (prefers-reduced-motion: reduce) {
.mqs-06__group {
animation: none;
}
.mqs-06__viewport {
overflow-x: auto;
}
.mqs-06__collapse {
transition: none;
}
}
```
## JavaScript
```js
/* No JavaScript — dismissal is a checkbox + :has(:checked) flipping grid-template-rows 1fr→0fr (the modern animate-to-height-auto trick). Add 2 lines of localStorage if dismissal should persist. */
```Here's a working CSS Infinite Marquee Design from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Flash Sale Announcement Bar — Fast, High-Contrast, Dismissible
Source: https://codefronts.com/motion/css-infinite-marquee/flash-sale-announcement-bar/
The urgency pattern e-commerce lives on: a fast, high-contrast marquee bar pinned to the top of the page shouting a limited-time offer, with a working promo-code link, a dismiss button that collapses the bar with pure CSS (:has() + a checkbox), and a grid-template-rows animation so the page content slides up smoothly instead of jumping.
## HTML
```html
<section class="mqs-06" aria-label="Flash sale announcement bar demo">
<input class="mqs-06__closeBox" type="checkbox" id="mqs-06-close">
<div class="mqs-06__collapse">
<div class="mqs-06__clip">
<div class="mqs-06__bar">
<div class="mqs-06__viewport">
<div class="mqs-06__group">
<span class="mqs-06__msg">Flash sale — 40% off everything ends tonight</span><span class="mqs-06__zap" aria-hidden="true">⚡</span>
<span class="mqs-06__msg">Use code <a class="mqs-06__code" href="#deal">BLITZ40</a> at checkout</span><span class="mqs-06__zap" aria-hidden="true">⚡</span>
</div>
<div class="mqs-06__group" aria-hidden="true">
<span class="mqs-06__msg">Flash sale — 40% off everything ends tonight</span><span class="mqs-06__zap">⚡</span>
<span class="mqs-06__msg">Use code <span class="mqs-06__code">BLITZ40</span> at checkout</span><span class="mqs-06__zap">⚡</span>
</div>
</div>
<label class="mqs-06__closeBtn" for="mqs-06-close" title="Dismiss announcement"><span aria-hidden="true">✕</span><span class="mqs-06__sr">Dismiss announcement</span></label>
</div>
</div>
</div>
<div class="mqs-06__page">
<p class="mqs-06__hint">↑ Page content — dismiss the bar and watch it glide up (pure CSS, no JS).</p>
</div>
</section>
```
## CSS
```css
.mqs-06,
.mqs-06 *,
.mqs-06 *::before,
.mqs-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.mqs-06 {
min-height: 100vh;
width: 100%;
--gap: 36px;
--speed: 14s;
font-family: 'Segoe UI',system-ui,sans-serif;
background: #fafaf9;
}
.mqs-06__closeBox {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.mqs-06__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.mqs-06__collapse {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows .3s ease;
}
.mqs-06:has(.mqs-06__closeBox:checked) .mqs-06__collapse {
grid-template-rows: 0fr;
}
.mqs-06__clip {
min-height: 0;
overflow: hidden;
}
.mqs-06__bar {
display: flex;
align-items: stretch;
background: #fde047;
border-bottom: 2px solid #1c1917;
}
.mqs-06__viewport {
flex: 1;
display: flex;
gap: var(--gap);
overflow: hidden;
padding-block: 11px;
}
.mqs-06__group {
flex: none;
display: flex;
align-items: center;
gap: var(--gap);
animation: mqs-06-scroll var(--speed) linear infinite;
will-change: transform;
}
@keyframes mqs-06-scroll {
to {
transform: translateX(calc(-100% - var(--gap)));
}
}
.mqs-06__viewport:hover .mqs-06__group,
.mqs-06__viewport:focus-within .mqs-06__group {
animation-play-state: paused;
}
.mqs-06__msg {
flex: none;
font-size: 13px;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
color: #1c1917;
}
.mqs-06__zap {
flex: none;
font-size: 12px;
}
.mqs-06__code {
color: #1c1917;
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-thickness: 2px;
border-radius: 3px;
}
.mqs-06__code:hover {
background: #1c1917;
color: #fde047;
text-decoration: none;
padding-inline: 4px;
}
.mqs-06__code:focus-visible {
outline: 2px solid #1c1917;
outline-offset: 2px;
}
.mqs-06__closeBtn {
flex: none;
display: grid;
place-items: center;
width: 42px;
font-size: 14px;
font-weight: 700;
color: #1c1917;
cursor: pointer;
border-left: 2px solid #1c1917;
transition: background .15s;
}
.mqs-06__closeBtn:hover {
background: #facc15;
}
.mqs-06__closeBox:focus-visible ~ .mqs-06__collapse .mqs-06__closeBtn {
outline: 2px solid #1c1917;
outline-offset: -4px;
}
.mqs-06__page {
padding: 56px 24px 64px;
text-align: center;
}
.mqs-06__hint {
font-size: 13px;
color: #a8a29e;
}
@media (prefers-reduced-motion: reduce) {
.mqs-06__group {
animation: none;
}
.mqs-06__viewport {
overflow-x: auto;
}
.mqs-06__collapse {
transition: none;
}
}
```
## JavaScript
```js
/* No JavaScript — dismissal is a checkbox + :has(:checked) flipping grid-template-rows 1fr→0fr (the modern animate-to-height-auto trick). Add 2 lines of localStorage if dismissal should persist. */
```How this works
The bar runs the duplicated-group loop at an urgent 14s with bold uppercase copy and a lightning glyph between repeats. Inside the moving track sits a real <a> for the promo code — and this is where :focus-within earns its keep: the instant the link receives keyboard focus, the whole bar pauses, so tab users can actually activate the moving link. Without that rule a focusable element inside a marquee is a WCAG failure waiting to happen.
Dismissal is pure CSS: a visually-hidden checkbox before the bar, a ✕ styled as its label, and .mqs-06:has(#mqs-06-close:checked) .mqs-06__collapse { grid-template-rows: 0fr }. The bar's content sits in a one-row grid whose row animates 1fr → 0fr — the modern height-auto animation trick — so the announcement squeezes shut in 300ms and the page below glides up with no layout snap.
High contrast is doing the marketing here: near-black text on a saturated yellow band hits ~13:1 contrast, so even at 14s the copy stays legible in peripheral vision — which is exactly how announcement bars are consumed.
Techniques used: fast-loop duplicated groups with focusable link + :focus-within pause · pure-CSS dismiss via :has(:checked) · animated height-auto collapse with grid-template-rows 1fr→0fr · interpolate-size-free technique (works today everywhere grid does) · 13:1 contrast band for peripheral legibility
Make it yours
- Urgency dial: --speed between 10s (aggressive) and 18s (calm); below 10s the copy becomes decoration, not information.
- Persist dismissal: this demo resets on reload by design; in production add 2 lines of JS to store the checkbox state in localStorage.
- Collapse duration: the .3s on grid-template-rows; pair it with an opacity fade for extra polish.
- Palette: yellow/black is the highest-recall combo; red bands should lighten the text track to keep ≥4.5:1.
Gotchas — read before shipping
- A link inside a marquee REQUIRES the :focus-within pause rule — moving focus targets are both a usability bug and a WCAG 2.4.7 problem.
- Animate grid-template-rows, not height — height:auto→0 doesn't transition (without interpolate-size), the grid row trick works everywhere today.
- min-height:0 (or overflow:hidden) on the grid child is mandatory or the row refuses to shrink below content size.
- Don't stack two moving bars (announcement + press logos) in one viewport-height — motion competes and both get ignored.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 105+ | 15.4+ | 121+ | 105+ |
:has() powers the dismiss; without it the ✕ simply doesn't collapse the bar (content unaffected). grid-template-rows transitions are baseline since 2023.