In short and in plain language
For Black Week, some shops add new parts. For example a bar at the top, a countdown or a coupon pop-up.
Often these parts appear only after loading. Then the page slides down.
The article shows how to avoid the jump. The space is kept free in advance. The server sends the bar with the page.
After the sale, the server takes the parts out. So no gap is left. Test before and during the sale.
We can help you with this.
For Black Week, almost every online shop gains extra elements: a notice bar above the header, a countdown to the end of the sale, a coupon pop-up and promo badges on the product tiles. They usually arrive shortly before launch through a script, a tag manager or a plugin – exactly when nobody has time left to measure their effect on the Core Web Vitals. Everyone knows the result from their phone: the page is there, the product image is visible, and a moment later everything slides down by the height of a bar. This article shows how to add seasonal promo elements so that Cumulative Layout Shift stays in the green – with reserved space, server-side delivery, a countdown that does not keep the main thread busy, badges without late-loading images and a clean-up after the sale that leaves no empty gap. In 2026, Black Friday falls on 27 November and Cyber Monday on 30 November. The weeks until then are enough to build the elements in advance and measure them with the sale switched on.
Key takeaways
- A good CLS is 0.1 (web.dev) or less, measured at the 75th percentile (web.dev) of page loads. A promo bar inserted at the top after rendering can use up that entire margin on a smartphone on its own.
- Reserve space for the bar, countdown and badges in CSS, for example with
min-heightoraspect-ratio. An element that appears inside space that is already reserved moves nothing and does not count as a layout shift. - The bar ships with the HTML from the server instead of being injected by a script after rendering. The server decides from start and end time, including time zone, whether the sale is on – so the very first frame is right.
- A countdown derives the remaining time from a fixed end time, writes only its own text and uses
tabular-nums. Tasks longer than 50 milliseconds (web.dev) on the main thread otherwise delay the response to clicks. - Coupon pop-ups sit above the content as an overlay with
position: fixed. Shifts during scrolling count as unexpected even if they happen within 500 milliseconds (web.dev) of the gesture. - After the sale, the server leaves the elements and their reserved space out of the HTML. Collapsing the space in the browser causes the same jump in reverse, and CrUX shows it across a window of 28 days (Chrome for Developers).
Why promo elements push content around
Seasonal elements come about differently from the rest of a shop page. The header, product tiles and cart are built once, tested and then maintained. The Black Week notice bar, the countdown and the coupon pop-up, on the other hand, are added for a few weeks, often at short notice and outside the normal development process: through a script in the tag manager, a plugin with its own interface or a snippet the marketing team inserts itself. Technically, it usually works the same way. The script loads after the HTML, waits for the document and then writes the bar to the start of the body. By that point, the browser has long since rendered the page – logo, navigation, product image and title are already in place. The bar pushes everything below it down, and anyone who was about to tap the product image hits something else.
This is exactly the movement that Cumulative Layout Shift measures. CLS captures the largest burst of unexpected layout shifts during the entire lifecycle of a page (web.dev). Such a burst, the session window, covers shifts that occur less than 1 second (web.dev) apart and lasts at most 5 seconds (web.dev). A value of 0.1 (web.dev) or less counts as good, a value above 0.25 (web.dev) as poor. It is measured at the 75th percentile (web.dev) of page loads, segmented by mobile and desktop. So a good average is not enough: three out of four page loads must not exceed the threshold. A bar that appears late only on slow connections hits precisely the slower visits that determine the value at the 75th percentile.
Across the web, the situation is better than its reputation: 81 percent (HTTP Archive Web Almanac 2025) of mobile pages and 72 percent (HTTP Archive Web Almanac 2025) of desktop pages achieved a good CLS in 2025. The difference between page types is telling. On mobile, 79 percent (HTTP Archive Web Almanac 2025) of home pages make it, but 81 percent (HTTP Archive Web Almanac 2025) of secondary pages. The authors read this as a sign that home pages, with their often more dynamic content – hero media, banners and promotional elements – remain more prone to layout shifts (HTTP Archive Web Almanac 2025). That is exactly where Black Week elements land first. How CLS comes about in general, for instance through images without dimensions, web fonts or ad slots, is covered in the article avoiding Cumulative Layout Shift in the shop. This one deals only with elements that arrive for a sale and leave again afterwards.
Why the timing matters
What a bar costs in CLS
Each individual shift receives a score made up of two factors: impact fraction times distance fraction (web.dev). The impact fraction is the area that all shifted elements occupy in the viewport before and after, combined, as a share of the viewport area. The distance fraction is the greatest distance any element has moved, divided by the viewport's largest dimension (web.dev). On a smartphone in portrait mode that is the height, on a desktop the width. New elements do not count as shifted themselves, but everything they push out of position does.
For a bar inserted at the very top, above everything else, the score is easy to estimate. It pushes the entire visible content down, so the impact fraction is close to one. The layout shift score then equals at most the bar height divided by the largest viewport dimension. The table shows a worked example based on this formula for a smartphone with a viewport height of 844 pixels and a desktop at 1440 × 900 pixels. The heights are typical sizes for promo bars, not measurements from a particular shop.
| Promo element, inserted at the top | Shift | Viewport | Layout shift score, at most |
|---|---|---|---|
| Single-line notice bar | 40 px | Smartphone, 844 px tall | 0.047 |
| Two-line bar on a smartphone | 64 px | Smartphone, 844 px tall | 0.076 |
| Bar with its own countdown line | 90 px | Smartphone, 844 px tall | 0.107 |
| Single-line notice bar | 40 px | Desktop, 1440 × 900 px | 0.028 |
Two things stand out. First, it does not take a large area: in the worked example, a bar with its own countdown line exceeds the threshold on its own. Second, shifts within the same session window add up. If the bar, the countdown text and a late-loading badge image arrive in quick succession, the sum counts. On desktop, the same bar has less effect because the distance fraction is divided by the width. Anyone who signs off the sale on a large screen only will therefore systematically underestimate the problem. Measure on a smartphone, with the text that will actually be in the bar during Black Week.
Often invisible in the lab
Reserving space: bar, countdown, badge
The first lever is unspectacular: the space a promo element needs is fixed in the very first layout. For late-loading content in the document flow, web.dev recommends exactly that – reserve the room with a min-height rule or, for responsive content, with aspect-ratio (web.dev). If a new element then appears in that room, it is not a layout shift as long as no other visible element moves as a result (web.dev). The bar may therefore arrive later, provided its space is already there. It just must not push anything aside.
The height has to fit the text. A bar that wraps onto two lines on mobile breaks a reservation for one line, and the jump sneaks back in. So settle the text in advance for the narrowest supported width and plan the reserved space per breakpoint: one line on desktop, two lines on a smartphone if the text needs them. min-height is only a lower bound here: text that is too long still makes the bar grow, even with overflow: hidden. So the text has to fit the reserved space, and the paragraphs inside need margin: 0; otherwise the default spacing alone makes the bar taller than planned. The same applies to the font as everywhere else on the page: if the web font loads late and runs wider than the fallback font, one line can turn into two.
/* Reserved space for the promo bar, fixed per breakpoint */
.promo-bar {
min-height: 40px;
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
column-gap: 0.5em;
}
.promo-bar p { margin: 0; }
@media (max-width: 480px) {
.promo-bar { min-height: 64px; }
}
/* Countdown: every digit equally wide, fixed frame */
.promo-countdown {
display: inline-block;
min-width: 12ch;
font-variant-numeric: tabular-nums;
}
/* Badge on the product tile: above the image, out of the flow */
.product-card { position: relative; }
.product-card .promo-badge {
position: absolute;
top: 8px;
left: 8px;
}The countdown causes a second, easily overlooked shift. In many fonts, digits have different widths; a one is narrower than an eight. If the display changes every second, the width of the text changes with it, and in a centred bar the neighbouring elements move back and forth. font-variant-numeric: tabular-nums activates figures of equal width that align as in tables (MDN Web Docs). A minimum width in ch additionally keeps the frame stable, even when the day count drops from two digits to one.
Promo badges on product tiles belong inside the tile's frame, not in its flow: positioned absolutely above the image or in a row that exists on every tile anyway. If the badge is delivered as an image, it needs width and height; otherwise the browser first lays it out without height and pushes the text below as soon as the file arrives. This is still widespread on the web: on 62 percent (HTTP Archive Web Almanac 2025) of mobile pages, at least one image lacks dimensions. The simplest option is a badge made of text and CSS. It needs no extra request, has no dimensions anyone can forget and can be translated. If you still need promo graphics as images, the article on JPEG XL in the browser covers the question of the right format.
Notice bar
Fixed height per breakpoint on the element itself, text written for the narrowest width, font loaded early. The bar fills its space, it does not create it.
Countdown
Equal-width digits via tabular-nums, a minimum width in ch, the starting value from the server. The display changes its content, but not its size.
Promo badge
Text and CSS instead of an image file, positioned absolutely above the product image. Where an image is unavoidable, width and height carry the dimensions.
Deliver from the server instead of injecting later
Reserved space only helps if it matches the sale. If a script in the browser decides whether the bar appears, the CSS needs a reservation even when no sale is running – or it has none and the jump returns. It is cleaner to ship the promo element with the HTML. The server knows the start and end time and renders the bar, the countdown's starting value and the badges only within that period. The browser then sees the bar in the first frame, and there is nothing that could shift. In a Shopware storefront, that means the bar is a template block with its own condition, not a script that writes into the body after loading.
The usual alternative is a bar that a tag manager or plugin inserts via JavaScript. It depends on script loading times that vary from visit to visit and therefore appears sometimes early, sometimes late. web.dev explicitly names banners and forms that pop in at the top or bottom of the viewport and shift the rest of the content as a typical cause. The recommendation is to reserve sufficient space in advance or to take the element out of the document flow as an overlay (web.dev). If a script really has to insert the bar, then into a container that is already in the HTML and carries its height.
<!-- Rendered by the server only while the sale runs, otherwise absent -->
<div class="promo-bar" data-promo-end="2026-12-01T00:00:00+01:00">
<p>Black Week: sale prices until Monday, midnight</p>
<p>Ends in <span class="promo-countdown">2 d 05:14:09</span></p>
</div>With server-side delivery, the question moves to the cache. An HTML cache or CDN that holds the home page for hours may deliver the bar too late or still show it after the end. Plan the start and end as events: purge the cache at the switch-over time or shorten the lifetime of the affected pages around that moment. How page caching and freshness get along is covered on the page about caching strategies. The second trap is time zones: “until Monday, midnight” means Europe/Berlin for a shop in Germany, not the server time in UTC. The end time belongs in the configuration with its time zone, not as text in the template.
The principle in one sentence
A countdown without constant load on the main thread
A countdown is a small piece of logic, but it can become expensive in two places: on the main thread and in accuracy. Interaction to Next Paint measures how quickly a page responds to clicks, taps and key presses. An INP of up to 200 milliseconds (web.dev) counts as good. Any task that runs longer than 50 milliseconds (web.dev) is a long task, and while it runs, every input waits. A countdown that re-renders an entire component every second, queries layout on every step or formats several time zones can contribute to such a task – and it runs exactly when visitors tap “Add to cart”.
Three rules keep it small. It writes only the text of a single node. It does not read layout while doing so. And it recalculates the remaining time from the fixed end time on every step instead of decrementing a counter. The last rule has to do with timers in the background. Browsers throttle them in inactive tabs: Chrome checks chained timers only once per minute (MDN Web Docs) when the page has been invisible for more than 5 minutes (MDN Web Docs) and further conditions are met. Firefox for Android applies a minimum delay of 15 minutes (MDN Web Docs) to inactive tabs. A countdown that counts ticks shows the wrong time after the visitor returns to the tab. One that calculates from the end time is correct again on the next step.
// Countdown: derives everything from the end time, writes text only
const el = document.querySelector('.promo-countdown');
const bar = el && el.closest('[data-promo-end]');
if (el && bar) {
const end = Date.parse(bar.dataset.promoEnd);
const pad = (n) => String(n).padStart(2, '0');
let timer;
const render = () => {
const rest = Math.max(0, end - Date.now());
const s = Math.floor(rest / 1000);
const text = `${Math.floor(s / 86400)} d ${pad(Math.floor(s / 3600) % 24)}:` +
`${pad(Math.floor(s / 60) % 60)}:${pad(s % 60)}`;
if (el.textContent !== text) el.textContent = text;
if (rest === 0) clearInterval(timer);
};
timer = setInterval(render, 1000);
render();
// Recalculate as soon as the tab is visible again
document.addEventListener('visibilitychange', () => {
if (!document.hidden) render();
});
}The server writes the starting value into the HTML so that the display shows a plausible time even before the script runs, and the script only corrects it. Whether a seconds display is needed at all is worth asking marketing: a display in hours and minutes changes less often and is usually sufficient for a sale that lasts days. In shop front ends with a component framework, the countdown gets its own small state rather than that of the whole header – otherwise every second re-renders the navigation as well. How to find and break up long tasks is shown in the article optimizing INP.
Coupon pop-ups: overlay instead of shift
The coupon pop-up is particularly prone to jumps because it rarely appears with the page and usually arrives with a delay: after a few seconds, while scrolling or when the pointer leaves the window. As an overlay with position: fixed, it sits outside the document flow and shifts nothing. web.dev describes the pattern using cookie notices as the example: a notice at the top of the document that is inserted after rendering pushes the elements below it down; a sticky footer or modal sits on top of the page as an overlay and should not cause shifts when it loads (web.dev). The same logic applies to coupon windows. How consent dialogs themselves affect the metrics is covered in the article cookie banners and Core Web Vitals.
Do not rely on the exemption for user input. Shifts within 500 milliseconds (web.dev) of a click, tap or key press are indeed excluded from the calculation. Scrolling, dragging and zooming, however, do not count as such input. For content that arrives while scrolling or hovering, web.dev puts it plainly: any shift during these interactions counts as unexpected, even if it happens within 500 milliseconds (web.dev). A pop-up or notice block pushed into the flow while scrolling therefore counts in full.
If the pop-up or the bar slides in with an animation, use transform and opacity. web.dev advises using transform: scale() instead of changing height and width, and moving elements with transform: translate() rather than via top, right, bottom or left (web.dev). Animations on layout properties are common: non-composited animations appear on 40 percent (HTTP Archive Web Almanac 2025) of mobile pages. A promo window that slides in from the top via margin-top shifts the page below it on every frame.
Close first, tidy up afterwards
After the sale: remove without a gap
The clean-up is the part that is usually missing from the plan. When the sale ends at midnight, two variants go wrong. Either the reserved space stays in the CSS and shows an empty area above the header – no layout shift, but a visible leftover that remains until the next release. Or a script hides the expired bar in the browser and collapses the space. web.dev explicitly warns against letting reserved space collapse: removing the space set aside for elements can cause just as much CLS as inserting content (web.dev).
The solution again lies with the server. From the end time onwards, it renders neither the bar nor the reserved space nor the badges. Anyone loading the page after the end gets a layout without promo elements. Anyone who loaded it before keeps the bar until the next page change – with a countdown that stops at zero. If the text there switches to “Sale ended”, it has to fit into the same height. What matters is that nothing collapses in the browser while someone is reading.
For this to work, the reserved space belongs to the element and not to the global layout. If min-height sits on the bar's class and the element is missing from the HTML, the reservation is missing too. If it sits as padding on the body or the header instead, it remains after the sale. The same goes for badges: they disappear with the next render of the tile, not through a script that removes them from finished pages. A clean switch-over looks like this:
- Store start and end time with time zone in the configuration, not as text in the template.
- Decide for both points in time how the HTML cache reacts: purge it selectively or shorten its lifetime beforehand.
- Define the reserved space exclusively on the classes of the promo elements, not on the page frame.
- After the end, reload the home page, a category and a product page on a smartphone and check that bar, reserved space and badges are gone.
- On the following day, check your own field data to see whether the values return to their pre-sale level.
Measure before and during the sale
Before launch, measure the page as it will look during Black Week, that is with the sale switched on. That requires a way to make the elements visible in advance: a preview parameter, a test channel or an earlier start date on the staging system. After recording a trace in the Performance panel of Chrome DevTools, the Layout Shifts track shows every shift; clicking it plays the shift back and shows the details (web.dev). Measure with a smartphone profile, a throttled connection and an empty cache, and use the page during the recording: scroll, wait for the pop-up, close it. Otherwise the tag manager script is already cached on the developer machine and appears too early to stand out. A technical analysis is a good way to take structured stock before the sale.
During the sale, field data is what counts. The Chrome UX Report is a rolling average over 28 days (Chrome for Developers). A jump that goes live on the first day of Black Week only shows up there gradually and stays in the window into December. For a quick response, you need your own measurement in the browser. The Layout Instability API reports every shift together with the elements that moved (MDN Web Docs) – that is, the displaced content, not the inserted bar itself. With a bar inserted at the top, the list therefore shows the header or the product image, and the trigger sits directly above it. MDN, however, does not list the interface as Baseline because it does not work in some of the most widely used browsers (MDN Web Docs), so the values do not cover every visit. For finding out which element jumps, that is sufficient.
// Log layout shifts with the first element that moved (Chromium browsers)
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue;
const node = entry.sources && entry.sources[0] && entry.sources[0].node;
const target = (node && node.className) || (node && node.nodeName) || 'unknown';
console.log(entry.value.toFixed(4), target, Math.round(entry.startTime));
}
}).observe({ type: 'layout-shift', buffered: true });In production, instead of logging to the console, send the largest value per page load to your own analytics, together with the page type and a flag showing whether the sale was active. That way you see the difference between pages with and without promo elements on the same day, without waiting for the CrUX window. How your own field data and CrUX complement each other is described in the article RUM or CrUX. Which measurements are permitted without consent is covered in the article on performance measurement without consent.
Checklist for Black Week
- Bar, countdown, pop-up and badges are on the staging system with the real text, the real font and the real start date.
- Every promo element in the document flow has a reserved height per breakpoint, defined on its own class.
- Coupon pop-ups and notice windows sit above the content with
position: fixedand animate onlytransformandopacity. - The bar ships with the HTML from the server; start and end are configured with time zone, and the cache is planned for both points in time.
- The countdown calculates from the end time, uses
tabular-nums, writes a single text node only and recalculates when the tab becomes visible again. - Badges are text and CSS or images with
widthandheight, positioned above the product image. - A recording in the Performance panel with a smartphone profile, throttled connection and empty cache shows no shifts caused by promo elements.
- Your own field measurement flags page loads with an active sale so that deviations show up on the same day.
Using the run-up to Black Week
Promo elements are rarely one-offs. The Black Week bar returns at Easter, for the summer sale and at Christmas, and so does the countdown. Build them once as a component with reserved height, a server-side time window and a lean countdown, and you can reuse them for every sale that follows. Black Week is a good occasion for this, because a lot is at stake – and at the same time a particularly bad moment for a fix in live operation. Alongside layout stability, the preparation should also cover whether server and shop can handle the rush; the article on load testing for peak season describes the approach. If there is also a carousel at the top of the home page, the article on hero sliders is worth a look.
For shops built on Shopware, the bar and badges can be implemented as template blocks with a time condition; the page on Shopware performance has notes on the storefront. If you would rather not check the promo elements yourself, we measure them as part of our Core Web Vitals optimisation with the sale switched on, on a smartphone, attribute every shift to its element and rebuild the elements so that they reserve space, come from the server and disappear without a trace after the end. Where the front end needs a more fundamental rebuild for this, front-end optimisation is the starting point.
Sources and studies
Related Articles
Break Up Long Tasks: Lower INP with scheduler.yield
Break long JavaScript tasks over 50 ms into chunks with scheduler.yield(), free the main thread between the chunks and keep the INP steadily under 200 ms.
Long Animation Frames: Find the Scripts Slowing INP
The Long Animation Frames API reveals since Chrome 123 exactly which script causes a slow INP interaction – so you optimize deliberately instead of blindly.
Cookie Banners and Core Web Vitals: Fixing the LCP Trap
Cookie banners move LCP, INP and CLS: how to measure the consent layer impact in the field, rebuild it properly and take it out of the critical path for good.