# Floating buy bars that cover the page when zoomed in

Source: https://easeweb.dev/learn/floating-buttons
Topics: Dialogs and overlays > Floating buttons never cover content (WCAG 2.4.11, 1.4.10)
The fix: Keep the bar no taller than the space kept for it, and out of the way when the window is short. Either keep it to one row at every width, cutting the name short, so the space kept for it always matches, or do that and also let it sit in the page, after the footer, when the window is under 30em tall, as it is at 400% zoom or on a phone held on its side.
Test with: zoom, keyboard
References: https://www.w3.org/WAI/WCAG22/Understanding/reflow.html https://www.w3.org/WAI/WCAG22/Techniques/css/C34 https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-padding https://developer.mozilla.org/en-US/docs/Web/CSS/text-overflow

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A clothing shop's product page is drawn for a phone 390 pixels wide: a photo, "Linen overshirt", $48, the colour, an Add to bag button, a short description, the details, and links to the size guide, delivery and returns, and contact. As the page scrolls, a white bar floats just above the bottom edge, rounded and softly shadowed, with the name, the price and Add to bag again, so the button is always a thumb away. The developer builds it as drawn and, knowing that bars like this can hide focus, keeps the bar's 5rem clear: scroll padding so focus stops above it, and padding at the end of the page so the last link can scroll above it. On the phone and on a laptop, it works.

Someone with low vision zooms the page to 400% in a window 1280 by 1024 pixels, which leaves the page 320 by 256 CSS pixels. At that width the name, the price and the button no longer fit in one row, and Add to bag drops to a second one. The bar is now 6rem tall, and with its offset it covers the bottom 7rem of a window 16rem tall: nearly half of it. The 5rem kept for it is 2rem short. Scroll to the end and Contact us stays under the bar, with no further to scroll. Tab to it, and focus is on a link nobody can see.

## Why it fails

The space kept for the bar was measured from the drawing, one row tall, but the bar's height depends on the width it gets. Nothing tells the reserve that the bar has grown. Success criterion 1.4.10 asks that content can be presented at a width of 320 CSS pixels without loss of information or functionality. Contact us is on the page, but at that width no amount of scrolling shows it, so it is lost, and so is the link. Success criterion 2.4.11 fails with it: when the link has focus, it is entirely hidden by the bar. The scroll padding tells the browser to bring focus to 5rem above the bottom edge, which the grown bar still covers, and at the end of the page there is nowhere left to scroll, so the browser does all it was told and the link stays hidden. That is failure F110, a sticky footer or header completely hiding focused elements.

The Understanding page for 1.4.10 warns about pinned content beyond what it hides: it can make reading difficult if not impossible, and the page strongly suggests that at smaller sizes such parts get static positioning, or can be hidden by the person using the page. Here the strip left to read in, above the bar, is 9rem: six lines of text.

## Who is affected

Anyone who sees the page at a small size in CSS pixels: zoomed in on a desktop, or with a large display size on a phone. A phone held on its side gives the bar a short window too, wide enough to keep it on one row but leaving little to read above it.

## What automation and AI miss

Scanners load the page at desktop size, where the bar fits on one row and the space kept for it matches. axe has no rule for it, and checks for hidden focus tab through the page at one window size. The failure needs a narrow window and a page scrolled to the end. EaseWeb's audit now looks for it: at 320 by 256 CSS pixels it scrolls to the end of the page and reports text or a control still under a fixed or sticky element. Design reviews look at the phone frame the bar was drawn in, 390 pixels wide, and rarely at 320. A bar written by an AI assistant can get `flex-wrap: wrap` to make it responsive, with the reserve copied from the height in the design.

## Before: a bar that wraps, and space kept for one row

```css
:root {
  /* The bar as drawn: 4rem tall, 1rem above the bottom edge. */
  --bar-space: 5rem;
}
html {
  scroll-padding-bottom: var(--bar-space);
}
body {
  padding-bottom: var(--bar-space);
}
.buy-bar {
  position: fixed;
  inset-inline: 1rem;
  bottom: 1rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  padding: 0.75rem 1rem;
  border-radius: 1rem;
  background: #fff;
}
.buy-bar-name {
  flex-grow: 1;
}
.buy-bar-add {
  margin-inline-start: auto;
  white-space: nowrap;
}
```

```html
<div class="buy-bar">
  <p class="buy-bar-name">Linen overshirt</p>
  <p class="buy-bar-price">$48</p>
  <button type="button" class="buy-bar-add">Add to bag</button>
</div>
```

The bar comes last in the page, after the footer, and is fixed to the bottom of the window. At 320 pixels the button wraps onto a second row, so the bar grows to 6rem, while the scroll padding and the padding at the end of the page still keep 5rem. The last 2rem of the page are under the bar at every scroll position.

## After: a bar that keeps to one row

```css
:root {
  /* The bar as drawn: 4rem tall, 1rem above the bottom edge. */
  --bar-space: 5rem;
}
html {
  scroll-padding-bottom: var(--bar-space);
}
body {
  padding-bottom: var(--bar-space);
}
.buy-bar {
  position: fixed;
  inset-inline: 1rem;
  bottom: 1rem;
  display: flex;
  align-items: center;
  gap: 0.5rem 1rem;
  padding: 0.75rem 1rem;
  border-radius: 1rem;
  background: #fff;
}
.buy-bar-name {
  flex-grow: 1;
  /* Cut the name short rather than wrap: it is the page's heading too. */
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.buy-bar-add {
  margin-inline-start: auto;
  white-space: nowrap;
}
```

```html
<div class="buy-bar">
  <p class="buy-bar-name">Linen overshirt</p>
  <p class="buy-bar-price">$48</p>
  <button type="button" class="buy-bar-add">Add to bag</button>
</div>
```

Without `flex-wrap: wrap`, the bar stays one row at every width, and the name gives up the room: at 320 pixels only its first few letters are left. The bar keeps the 4rem it was drawn at, so the space kept for it stays true, and every line of the page, Contact us included, can be scrolled clear of it. The full name is still the page's heading, so nothing is lost. The look on the phone and the desktop doesn't change. What it doesn't change either is the share of the window: at 400% the bar still covers 5rem of a window 16rem tall, almost a third, and people read through what is left. Keep the bar's other text, and its button's label, short enough to fit one row beside the price at 320 pixels.

## After: pinned only when the window is tall enough

```css
:root {
  /* The bar as drawn: 4rem tall, 1rem above the bottom edge. */
  --bar-space: 5rem;
}
html {
  scroll-padding-bottom: var(--bar-space);
}
body {
  padding-bottom: var(--bar-space);
}
.buy-bar {
  position: fixed;
  inset-inline: 1rem;
  bottom: 1rem;
  display: flex;
  align-items: center;
  gap: 0.5rem 1rem;
  padding: 0.75rem 1rem;
  border-radius: 1rem;
  background: #fff;
}
.buy-bar-name {
  flex-grow: 1;
  /* Cut the name short rather than wrap: it is the page's heading too. */
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.buy-bar-add {
  margin-inline-start: auto;
  white-space: nowrap;
}
/* A short window, at 400% zoom or on a phone on its side: the bar sits in the page. */
@media (max-height: 30em) {
  :root {
    --bar-space: 0;
  }
  .buy-bar {
    position: static;
    margin: 1rem;
  }
}
```

```html
<div class="buy-bar">
  <p class="buy-bar-name">Linen overshirt</p>
  <p class="buy-bar-price">$48</p>
  <button type="button" class="buy-bar-add">Add to bag</button>
</div>
```

The bar keeps to one row, as in the fix before, so wherever it floats, the space kept for it matches. Below 30em of window height (480 pixels at the default text size), it stops floating and sits at the end of the page, after the footer, where it scrolls like everything else, and the space kept for it goes to zero. At 400% zoom in a 1280 by 1024 window, the page is 256 pixels tall, so the whole window is the page's again. On a phone held upright, and on a laptop, the bar floats as drawn. This is technique C34, un-fixing sticky content with a media query; the page's own Add to bag button is still where it always was, near the top.

## Which option to use

Both keep the end of the page clear of the bar at every size, so both pass 1.4.10 and 2.4.11. Keeping the bar to one row is the smallest change, one line of CSS removed and four added, and a shorter name in the bar, but at 400% the bar still covers almost a third of the window, which is why it is marked as not enough on its own. Pinning the bar only when the window is tall enough is recommended: it is the same change plus one media query, and in a short window it gives the person all of the screen to read, while the phone and desktop look stays as drawn.

For the designer, that means two more frames in the design file. Draw the bar at 320 pixels wide, with the name cut short, and write the bar's height beside it, so the developer keeps exactly that space. Draw the window at 320 by 256 pixels, the size 400% zoom leaves, with the bar sitting after the footer. Keep the bar to one row of short things: a price and a button fit at 320 pixels; a photo, a size picker and a name don't.

## When the bar comes from a third party

Bars added by a shop platform's app or a third-party script often can't be restyled. Look for the vendor's settings for small screens, override its position from your own stylesheet if the markup is stable, and test again after each update.

## Implementation decisions

Count everything that floats over the bottom of the window, because the space kept has to cover them all, and each is another thing to check at 400%:

- a chat button above the bar, which needs its own corner kept as in [floating chat buttons that cover part of keyboard focus](/learn/focus-not-covered);
- a back to top button, which usually appears only after scrolling;
- a cookie banner, which on a first visit sits over the bar and adds its own height, and comes last in the page as in [cookie banners that hide keyboard focus and come last](/learn/cookie-banners);
- a sticky header, which takes the top of the same short window, as in [sticky headers and banners that hide keyboard focus](/learn/focus-not-obscured).

When the name has to show in full, keep the wrap and measure the bar instead: set `--bar-space` from the bar's rendered height with a `ResizeObserver`, adding its offset, so the reserve grows with it. That keeps the end of the page clear, but the bar takes even more of a short window, so pair it with the media query. `position: sticky` with `bottom: 1rem`, on a bar that is the last child of `body`, is another way to keep the end of the page clear: a sticky bar keeps its own place in the page, and settles there at the end. Focus in the middle of the page still needs `scroll-padding-bottom` to match its height.

In a short window, hiding a bar that only repeats the page's own button, with `display: none`, works as well as moving it, as long as everything in it is also on the page. A button that puts the bar away is the other answer the Understanding page names; it needs a name ("Hide"), a place near the start of the bar's own Tab order, and to keep the bar away for the rest of the visit. Both answers want the bar to keep to one row while it floats.

The breakpoint is in `em`, which in a media query means the browser's default text size, so it follows that setting, not the page's CSS; browser zoom shrinks the window in CSS pixels and so reaches it. Pick the height at which the bar and a useful amount of the page both fit: 30em leaves the bar floating at 200% zoom in a window 1024 pixels tall.

A chat panel that opens over the page is its own floating part, which must also close from the keyboard and read out its messages: see [chat widgets that never announce their messages](/learn/chat-widgets). Where the bar is written in the markup decides when keyboard and screen reader users reach it, and a panel it opens has to come next after it, as in [floating buttons and panels that keyboard focus reaches last](/learn/floating-order).

## Verify the fix

1. In a window 1280 by 1024 pixels, zoom the browser to 400% (or set the viewport to 320 by 256 CSS pixels in the developer tools). The bar is one row, or sits in the page.
2. Scroll each page template to the end. Every line, the last link included, can be scrolled clear of the bar and of every other floating button.
3. Press Tab from the top of the page to the last control, and Shift + Tab back. The focused control is never entirely hidden.
4. Repeat on a phone held upright and on its side, in a window 320 pixels wide and 568 tall, and with the browser's text size at 200%.
5. Repeat with every floating part showing: the chat button, the back to top button, the cookie banner before it is answered, and any promotion bar.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the bar, its text or the page layout changes.

## Limits

This is a starting pattern, not a guarantee for every application. The 30em breakpoint and the bar's sizes are this example's; test your own bar with your longest product name, price and translation. A page that scrolls sideways at 320 pixels is a different failure, covered in [pages that scroll sideways on a narrow screen](/learn/reflow). Passing at 400% doesn't make the bar easy to use at 400%: check that the page's own Add to bag button is easy to find once the bar sits in the page. Class names and tokens in the snippets are placeholders for your own.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
