Banners Causing Layout Shift
Bars and banners are small, but they tend to sit at the very top of the page, so when one appears, changes height or vanishes, everything below it moves.
The usual offenders are an "added to basket" message slotted in above the product, a promotional strip injected by a script or app, a site-wide alert about delivery delays, a header that shrinks as you scroll, and a dismissed banner that flashes and then disappears. This guide shows how to tell which bar is responsible and how to make each one either float over the page or keep a fixed height from the first paint.
Why bars do so much damage
A layout shift is scored on how much of the screen moves and how far. A strip only 40 pixels high does not sound like much, but when it is added above the header it moves the header, the hero image, the headline and the first paragraph: nearly the whole viewport. On a phone, those 40 pixels are a large fraction of the screen height. A single bar arriving late can use up most of the 0.1 allowance for a good CLS score.
Bars also sit where people are about to tap. A visitor reaching for the menu, the search box or the "Add to basket" button can end up pressing something else when a strip pushes it down. That is the real cost, and it is why these shifts are worth fixing even when the score is borderline.
The five patterns
| Pattern | What you see | Where it comes from |
|---|---|---|
| Injected announcement bar | The page draws, then a promo strip appears at the top and everything drops | A marketing app, tag manager or script that adds the bar after load |
| Basket confirmation in the flow | After pressing "Add to basket", a message appears above the product and the button moves | Theme or cart script inserting a notice into the page instead of over it |
| Dismissed bar flash | A returning visitor sees the bar for a moment, then it disappears and the page jumps up | Dismissal stored in the browser and checked by a script after the first paint |
| Changing text | A countdown, basket counter or "spend ยฃX more for free delivery" message changes length and wraps onto another line | Text updated by script inside a box with no fixed size |
| Shrinking header | As you scroll, the header gets slimmer and the content under it jumps | A sticky header whose height is animated while it still takes up space in the page |
The half-second rule and basket messages
Shifts that happen within 500 milliseconds of a click, tap or key press are left out of CLS, on the basis that the visitor asked for the change. People often assume that makes "added to basket" messages harmless. It does not, for two reasons.
- The basket request is often slower than half a second. Most themes send the item to the server and wait for the reply before showing the message. On a phone connection that easily takes longer than 500 milliseconds, so by the time the message is inserted the grace period is over and the shift counts.
- Follow-up changes are not tied to the click. A cart drawer that opens, then a recommendations strip that loads inside the page a second later, then a free delivery bar that updates its text: only the first of these may fall within the window.
The safe design is a message that never moves anything in the first place: a toast in a corner, a slide-in drawer or a fixed bar that sits over the page. Then it does not matter how long the server takes.
Pinning down which bar is moving the page
- Run the CLS Debugger on the home page, a category page and a product page. If the elements it names are the header, the hero and the first headings, and the shift happens early, a bar at the top is the prime suspect.
- In Chrome DevTools, record a reload in the Performance panel with slow throttling. Select the shift and look at what sits above the elements that moved.
- Record a second session in which you add an item to the basket on a product page. Look for shifts starting more than half a second after the click; the Performance panel marks the click so you can measure the gap.
- Repeat as a returning visitor: dismiss the bar, reload, and watch whether it appears briefly before disappearing.
- Scroll the page slowly while recording, to catch a header that changes height as you scroll.
If you suspect a script or app is injecting the bar, view the page source (not the Elements panel). If the bar is not in the source, it is being added after load.
The fix, step by step
Step 1
Catch the bar in the act
Run the CLS Debugger on your main templates and record throttled reloads in Chrome DevTools, once as a new visitor, once after dismissing the bar, once while adding a product to the basket and once while scrolling. For each shift, note the bar, alert or header above the elements that moved and whether it was added, removed or resized. View the page source to see whether each bar is in the HTML or injected later.
Step 2
Render announcement bars in the HTML at a fixed height
Output promo strips and site-wide alerts in the server-rendered HTML, not
through a script, app embed or tag manager that adds them after load. Give the bar a fixed height
or min-height per breakpoint, keep messages to one line with white-space: nowrap
and text-overflow: ellipsis on narrow screens, and rotate multiple messages with opacity
or transform inside that same fixed box.
Step 3
Show basket confirmations as overlays
Change
"added to basket" messages so they appear as a fixed-position toast, a slide-in cart drawer or a bar
pinned over the top of the page, never as an element inserted above the product. Animate them in with
transform. Mark the message container with role="status" so screen readers
announce it, keep it clear of the main buttons on phones, and let it close automatically or with a clearly
labelled button.
Step 4
Hide dismissed banners before the first paint
When a visitor closes a bar, store the choice in a cookie your server reads, and leave
the bar out of the HTML on later visits. If the page is cached and served to everyone, use a tiny inline
script at the top of the head that reads the stored choice and adds a class to the html
element, with CSS that hides the bar when the class is present. Either way, the bar never appears and
disappears.
Step 5
Keep counters and changing text the same width
Give the basket count badge a min-width wide enough for two digits,
and use font-variant-numeric: tabular-nums on countdowns and prices so each digit takes
the same space. For free delivery messages whose length changes, reserve a fixed height and test the
longest wording at the narrowest phone width. Where a message could wrap onto two lines, shorten it so
it always fits on one.
Step 6
Stop sticky headers changing the page height
If the header shrinks on scroll, make the outer header box keep one height and shrink
only its contents with transform: scale() or by adjusting inner padding within that fixed
box. Alternatively, fix the header in place with position: fixed and give the page a matching
padding-top in the stylesheet from the start, so no content below depends on the header's
current height.
Step 7
Retest the whole shopping journey
Clear page and CDN caches, then repeat every recording from the first step: new visitor, returning visitor with the bar dismissed, adding to the basket on a slow connection, and a slow scroll. Re-run the CLS Debugger on each template. Test at a small phone width, where bars wrap most easily, and check that toasts and drawers do not cover the checkout button when they appear.
Code patterns
A fixed-height announcement bar
<div class="announce" role="region" aria-label="Announcements">
<p>Free UK delivery on orders over ยฃ40 at Fernhill Pantry</p>
</div>
.announce {
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.announce p {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
A basket toast that floats over the page
<div class="toast" role="status" aria-live="polite"></div>
.toast {
position: fixed;
left: 50%;
bottom: 1rem;
transform: translate(-50%, 150%);
transition: transform 0.25s ease;
}
.toast.is-visible { transform: translate(-50%, 0); }
Put the empty toast container in the HTML from the start and change only its text and class. Screen readers announce changes to an existing live region more reliably than a region that is added at the same moment as its message.
Hiding a dismissed bar before it is drawn
<head>
<script>
try {
if (localStorage.getItem('announceClosed') === '1') {
document.documentElement.classList.add('announce-closed');
}
} catch (e) {}
</script>
<style>.announce-closed .announce { display: none; }</style>
</head>
Because the script runs in the head before the body is drawn, the bar is never shown to someone who
closed it. The try block stops a blocked storage setting from breaking the page.
Platform notes
- Shopify. Most themes include an announcement bar section rendered in the HTML, which is usually stable. Bars added by third-party apps are often injected by script after load. Theme cart notifications vary between themes: some insert a notice into the page, others open a drawer. Theme and app specifics are in CLS in Shopify.
- WordPress and WooCommerce. WooCommerce's standard "has been added to your basket" notice is part of the page when the product page reloads after adding. Themes and plugins that add to the basket without reloading often insert their own notices; check how they are positioned. Notification bar plugins differ in whether they push content or float. See CLS in WordPress.
- Custom sites and headless shops. You control the markup, so put the bar in the server-rendered HTML and use a single toast component for all feedback messages.
Cookie notices, newsletter prompts and age checks are a different family of late content, with their own fixes in fixing CLS from dynamic content insertion.
Keeping bars in check
- One place for site-wide messages. Give the marketing team a single announcement bar in the theme to edit, so they do not add a second one through an app or tag manager when they need a new promotion.
- One component for feedback. Basket confirmations, form success messages and errors that are not tied to a field should all use the same floating toast.
- Write short messages. Agree a character limit for bar messages that fits the narrowest phone on one line.
- Test before seasonal campaigns. Sales periods bring new bars, countdowns and banners. Run the CLS Debugger on the campaign pages before they go live.
1. Does an announcement bar at the top of my site hurt CLS?
Not if it is part of the HTML from the start and keeps the same height. The browser draws it in the first paint with everything else, so nothing moves. It becomes a problem when a script or app adds it after the page is drawn, when its height changes because a rotating message wraps onto two lines, or when it is removed by a script for visitors who dismissed it. Any of those pushes the whole page up or down.
2. Do add to basket messages count as layout shift if the visitor clicked the button?
Only the changes that happen within 500 milliseconds of the click are excluded. Many shops send the item to the server and wait for a reply before showing the message, and on a mobile connection that can take longer than half a second. A message inserted into the page after that point counts in full. Showing the confirmation as a floating toast or a drawer avoids the question entirely, because it does not move anything whatever the timing.
3. Why does my promo bar flash and then disappear for returning visitors?
The page is drawn with the bar showing, then a script checks whether the visitor has closed it before and removes it. The removal moves all the content up and counts as a layout shift. Fix it by deciding before the first paint: read a cookie on the server and leave the bar out of the HTML, or use a tiny script at the top of the head that adds a class which hides the bar with CSS before anything is drawn.
4. Can a sticky header cause layout shift?
A header that stays the same size while sticking to the top of the screen does not. The trouble comes from headers that shrink or grow as the visitor scrolls, because shifts during scrolling count. If the header is still part of the page's normal flow, changing its height moves all the content beneath it. Keep the outer header box at a constant height and change only its contents, or fix it in place with matching padding on the page.
5. Are toast notifications better than banners for accessibility?
They can be, if built carefully. A toast in a fixed position does not move the content a visitor with a magnifier or screen reader is working through. Use a container that is always present in the page with role status, so assistive technology announces new messages. Keep toasts on screen long enough to read, let people close them, and do not use them for errors that need action, which belong next to the relevant form field.
6. How do I stop a countdown timer from causing layout shift?
Countdowns change their text every second, and in most fonts the digits are different widths, so the text can change length and push neighbouring words or wrap onto another line. Set font-variant-numeric to tabular-nums so every digit takes the same space, give the timer a fixed width, and make sure the full message fits on one line at the narrowest phone width. Render the timer's box in the HTML so it does not appear late.
7. Can a free delivery progress bar cause CLS?
Yes, when the message changes length as the basket total changes, for example from spend ยฃ12.50 more for free delivery to you have qualified for free delivery. If the text wraps onto a second line at some screen widths, the bar grows and moves everything below it. Reserve a fixed height, write each variation short enough for one line on a small phone, and update the progress indicator with a transform or width inside a container whose size does not change.
The CLS Debugger records every layout shift while your page loads, names the elements involved, calculates the cumulative score and ranks each shift by its impact, so you can see when a bar or header is pushing your content around.
Open CLS Debugger Fixes โ