Fixes9 min read·

Fixed headers and bottom bars on mobile Safari and Chrome

Sticky or fixed? How Safari and Chrome toolbars move on phones, keeping bottom bars clear of the home indicator and keyboard, and fixing hidden anchor links.

On phones, use position: sticky for headers and position: fixed for bottom bars, and keep both slim: on first load an iPhone 17 Pro shows only 718px of your page in Safari, and every pixel of bar comes out of that. Pad bottom bars with env(safe-area-inset-bottom) so they clear the home indicator, decide what happens when the keyboard opens, and add scroll-padding-top so anchor links don't land under the header.

Fixed or sticky?

Both keep an element on screen while the page scrolls. They get there differently:

position: sticky position: fixed
Takes up space in the page Yes, like a normal element No, content slides underneath
Needs page padding to avoid covering content No Yes
Positioned against Its nearest scrolling ancestor, within its parent The viewport
Best for Headers, section headings, table headers Bottom bars, floating buttons

For a header, sticky is nearly always the better choice:

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;       /* stay above page content */
  background: #fff;  /* don't let content show through */
}

When sticky "doesn't work", it's one of three things, all described on MDN's position page:

  1. No top. Without an inset value, sticky behaves like relative.
  2. A short parent. A sticky element only sticks while its parent is on screen. If the header is wrapped in a <div> that's exactly as tall as the header, it scrolls away with it. Make the header a direct child of body or of your full-height layout wrapper.
  3. An ancestor with overflow. overflow: hidden, auto or scroll on any ancestor makes the header stick inside that element instead of the page. The classic culprit is overflow-x: hidden added to stop sideways scrolling; How to fix horizontal scrolling explains why overflow-x: clip is the safer choice.

Fixed has its own trap: if any ancestor has a transform, perspective or filter other than none, that ancestor becomes the fixed element's frame of reference and your "fixed" bar scrolls with it. Keep fixed bars outside animated or transformed wrappers.

How the browser toolbars move

Mobile browsers change size while people scroll. These are doesitfit's first-load values, with the toolbars fully expanded as they are when a page opens:

Browser (first load) Above the page Below the page After scrolling down
Safari on iPhone 17 Pro, iOS 26 compact tab bar 62px status bar 94px Safari bar Safari's bars shrink; the page gains about 36px
Safari on iPhone SE 20px status bar 64px Safari bar Bars shrink as you scroll
Chrome on Galaxy S25 40px status bar + 56px toolbar 24px gesture bar Toolbar and gesture chin hide; the page gains about 80px

Status bars vary by model: 47–62px on Face ID iPhones and 58px on a Pixel 10. The resulting visible heights for every iPhone are in How much of the screen iPhone users see in Safari.

What this means for your bars:

  • Fixed elements follow the visible area. A bottom: 0 bar stays glued to the bottom of what's visible as the toolbars come and go. Chrome's write-up on URL bar resizing spells it out: vh units stay put, but fixed elements keep resizing as the bar shows and hides.
  • In Chrome, the top moves too. As Chrome's 56px toolbar slides away, a sticky header rides up to the new top of the screen.
  • In Safari, your bottom bar stacks on Safari's. On an iPhone 17 Pro that's 94px of Safari plus, say, 72px of your "Buy now" bar before anyone scrolls.
  • Safari 26 may colour behind its bars. Page content shows softly through Safari's blurred toolbars. When a fixed or sticky element sits against an edge next to them, Safari fills in behind its bars with a solid colour so no gap appears while scrolling, as a WebKit engineer explains in WebKit bug 301756. If an unexpected colour appears behind the status bar, look at your fixed header.

Budget the bars

On an iPhone 17 Pro, a 64px sticky header and a 72px bottom bar leave 718 − 64 − 72 = 582px of page. Rotate the phone and there's only about 337px to start with (see Landscape on phones). Rules of thumb:

  • One bar that stays on screen, not three. Header or bottom bar, rarely both.
  • Keep a sticky header at 56px or less on phones.
  • Cookie banners count too. Keep them short and dismissible.

Bottom bars and the home indicator

Face ID iPhones have a home indicator along the bottom edge, and Android phones with gesture navigation have a gesture bar. If your viewport tag includes viewport-fit=cover, the page can reach all the way down to them, and env(safe-area-inset-bottom) tells you how much room they need. It's 0 where nothing is in the way, so it's safe to add everywhere:

.bottom-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10;
  padding: 12px 16px;
  padding-bottom: calc(12px + env(safe-area-inset-bottom));
  background: #fff;
}

/* Leave room so the bar never covers the last bit of the page */
body {
  padding-bottom: calc(72px + env(safe-area-inset-bottom));
}

Use calc() (add) rather than max() here, so your buttons keep their 12px of breathing room above the indicator. Put the inset on the bar itself: a fixed element ignores its parent's padding. The side insets, landscape and the difference between max() and calc() are covered in Safe areas on iPhone.

Chrome on Android changed here in version 135: the viewport now extends into the gesture bar, and safe-area-inset-bottom updates live as Chrome's bottom "chin" moves out of the way. Chrome also added env(safe-area-max-inset-bottom), a constant you can size the bar with so it doesn't change height mid-scroll. Chrome's edge-to-edge guide recommends that on Android over the padding above, which it says makes the bar re-layout and keeps the chin from sliding away, and has a complete pattern.

When the keyboard opens

Tap into a form field and the on-screen keyboard slides up over a large part of the screen. In Safari, and in Chrome on Android since version 108, the keyboard shrinks only the visual viewport (the part you can see), not the layout. That keeps the page from reflowing, but it means "elements that use position: fixed remain in place and can be obscured" by the keyboard, in the words of Chrome's announcement. Firefox for Android made the same default in version 132.

You have three options.

1. Get the bar out of the way while typing. For a "Buy now" or navigation bar, this is the simplest fix, in CSS alone:

/* On touch screens, hide the bottom bar while someone is typing */
@media (hover: none) {
  body:has(:is(input, textarea, [contenteditable]):focus) .bottom-bar {
    display: none;
  }
}

Don't use this on a bar that contains the text field itself, like a chat composer.

2. Resize the layout, on Android. The viewport tag's interactive-widget key changes what the keyboard does:

<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">

With resizes-content, the layout shrinks to the space above the keyboard, so a bottom: 0 bar sits on top of it and dvh units shrink too (MDN). It works in Chrome on Android from version 108 and Firefox for Android from version 133. Safari doesn't support it yet: as of September 2026 it's implemented in WebKit's source but not shipped (Bramus's round-up). More on the options in The meta viewport tag.

3. Track the keyboard in JavaScript. For chat apps that must work on iPhone, the Visual Viewport API reports the visible height and fires resize events as the keyboard opens, so you can move the composer yourself. It's more work, so reach for it only when options 1 and 2 aren't enough.

Click a link to #pricing and the browser scrolls the "Pricing" heading to the very top of the viewport, right under your sticky or fixed header. One line on the scrolling element fixes every anchor on the page:

:root {
  --header-height: 56px;
}

html {
  scroll-padding-top: calc(var(--header-height) + 8px);
}

scroll-padding defines the part of the viewport where scrolled-to content should land. It lives in the scroll snap spec but, as MDN puts it, it applies to all scroll containers, snapping or not, and has worked in every major browser since 2021. If your page scrolls inside a wrapper (common in app layouts with height: 100dvh), put it on that wrapper instead of html.

For one-off targets, scroll-margin-top on the element does the same job from the other side:

h2[id] {
  scroll-margin-top: 72px;
}

The CSS Scroll Snap spec says browsers should use this margin when navigating to a fragment even when snapping is off, and both properties apply to scrollIntoView() as well. In Tailwind, that's scroll-pt-20 on <html> (80px) or scroll-mt-20 on the target.

There's an accessibility bonus. People tabbing through a page with a keyboard can land on a link hidden under a fixed bar; W3C's technique C43 uses scroll-padding to keep focused items visible, for WCAG's "Focus Not Obscured" criterion. Use scroll-padding-bottom for a fixed bottom bar.

Testing it

  • In doesitfit: turn on Browser bars to draw Safari's and Chrome's toolbars at the first-load heights above, then turn on Sync scroll (it needs doesitfit's measure.js snippet on your page, or the local proxy) and scroll: you'll see your header and bottom bar on every phone at once, and how much page is left between them. Safari's bar includes the home indicator; Rotate shows the landscape squeeze.
  • On a real phone: load the page fresh, scroll down and back up, tap an anchor link, and tap into a form field to see what the keyboard does to your bottom bar. The keyboard only exists on a real device. How to open localhost on your phone gets your dev build there.

Does your site fit?

Paste your address (or localhost:3000). doesitfit shows it on real screen sizes at once and tells you, bluntly, where it spills.

Free, no sign-up. Or ask Claude, ChatGPT or Codex to check it.

A prompt for your AI tool

Review my site's header and any bars fixed to the bottom of the screen on phones. Make the header position: sticky; top: 0 instead of fixed if possible, at most 56px tall on phones, with a z-index and background; if it stays fixed, pad the page so it doesn't cover content. Check that no ancestor of the sticky header has overflow: hidden, auto or scroll, and that no fixed bar sits inside an element with a transform or filter. Give bottom bars padding-bottom: calc(12px + env(safe-area-inset-bottom)) and give the body matching bottom padding. Hide non-essential bottom bars while a text field is focused on touch screens using body:has(:is(input, textarea, [contenteditable]):focus). Add scroll-padding-top to html equal to the header height plus 8px, using a CSS variable. Show me each change.

What to remember

  1. Sticky for headers, fixed for bottom bars. Sticky needs top, a tall parent and no overflow on its ancestors.
  2. Budget the bars against the visible height, about 718px on an iPhone 17 Pro on first load and far less in landscape. One bar, kept slim.
  3. Fixed bars follow the toolbars as they move, and in Safari they stack on top of Safari's own bar.
  4. Pad bottom bars with calc(12px + env(safe-area-inset-bottom)) on the bar itself.
  5. Plan for the keyboard: hide the bar while typing, or use interactive-widget=resizes-content where it's supported.
  6. Add scroll-padding-top so anchor links and keyboard focus land below the header.

Questions people ask

Should a header be position: fixed or position: sticky?

Usually sticky. It stays in the normal flow, so you don't need to pad the page to stop it covering content, and it still sticks to the top as you scroll. Fixed is the right tool for bars pinned to the bottom of the screen.

Why doesn't position: sticky work on my header?

The usual causes are a missing top value, a parent element that is only as tall as the header, or an ancestor with overflow set to hidden, auto or scroll, which makes the header stick inside that element instead of the page.

How do I stop anchor links scrolling under my fixed header?

Add scroll-padding-top to the html element, set to the header's height plus a little room. For individual targets you can use scroll-margin-top on the target element instead.

Why does the keyboard cover my fixed bottom bar?

When the on-screen keyboard opens, Safari and Chrome shrink only the visual viewport, so fixed elements stay where they were and the keyboard can cover them. Chrome and Firefox on Android can change that with interactive-widget=resizes-content; Safari doesn't support it yet.

Keep reading