react-fixed-bottom: usable Safari fixed bottom elements

Esaú Suárez Ramos
Turo Engineering
Published in
4 min readMay 14, 2019

Many design patterns, for instance material design with their FAB pattern and iOS with their bottom navigation bars, require components to be visually stuck to the bottom of the viewport.

FAB on the left, bottom bar on the right.

TL;DR: Safari mobile is a hostile environment: react-fixed-bottom makes fixed-bottom menus usable again.

The problem

This is web development and, as such, we need to think of all the edge cases in different browsers.

Naïve solution

Achieving this effect is very simple indeed (at least in 95% of the mobile browsers), we basically need to rely on some simple CSS rules that will set an element’s position to fixed and then offset it from the edge of the screen accordingly. We can then use this class on any HTML element and it will be positioned as expected.

CSS

.is-stuckToBottom {
position: fixed;
bottom: 20px; // A sensible offset from the edge of the viewport
}

HTML usage

<div class=“is-stuckToBottom">
<!-- Content -->
</div>

Real world TM

Easy, right? Not quite: there’s an actor in the play who’s here to stay and who’s a tad quirkier than just that: Safari mobile. Even though Apple is one of the first promoters for the bottom bar pattern, Safari is a hostile environment for achieving a proper implementation. Why? See the following animation:

Safari mobile overflow bar

What you can see in the animation above is a native iOS app behavior in Safari. When tapping and moving up, the bottom overflow bar is shown in the document. Its behavior, however, is not completely predictable: Safari shows the overflow bar using one of two strategies:

Safari Rendering Mode 1

Rendering inside the viewport. In this scenario, the content of the overflow bar is rendered inside the viewport and it is taken into account when offsetting elements from the bottom of the page. When this happens, there’s nothing to worry about because fixed elements react to it and are properly repositioned.

Safari Rendering Mode 2

Rendering in a layer above the viewport. Safari, as a native app, renders two native elements:

  • The first element is a container for the viewport content. Here’s where the HTML document is rendered.
  • The second element is rendered below the first one, outside of the HTML document.
  • The second element has a 44px height and it’s a bar at the bottom of the page. This bar is very likely to partially hide any fixed-bottom element.

Gathering information

When the rendering mode 2 mentioned above happens, we definitely have a problem to solve. Which resources can we use to solve it? These are the facts I found out after poking around with Safari console before, during and after the dreaded overflow bar appears:

  • Showing the overflow bar will likely* be a result of a scroll event in the document.
  • Showing the overflow bar won’t trigger a reflow; elements won’t change their position.
  • window dimensions DO change. Even though there is no reflow, the viewport is effectively shrunk.
  • HTML elements with a position: fixed; attribute keep their position.
  • As a consequence to the previous point (and the key to the solution) element.getBoundingClientRects() will yield a bottom offset outside of the window bounds after the overflow bar is rendered.

* Clicking near the bottom of the screen will also show the overflow bar. The behavior for this action is always scenario 1, though, so we don’t have anything to fix here.

The solution (vanilla)

This is a demo snippet on how to solve the issue in vanilla JS. This solution can be ported to any JS framework such as React, Vue or Angular, but I think it’s interesting to understand the fundamentals before adapting it.

The Vanilla way

TL;DR: add a scroll handler to check whether the element bottom property is currently out of the viewport: if it is, add an extra 44px bottom offset to prevent it.

The solution (React)

We love React (looks like everyone does nowadays), so we combined our previous learnings with React and we ended up with an elegant component.

The React way

The highlights here are:

  1. We use a React ref on a different div. By doing this, we don’t interfere with existing refs in the children component.
  2. Calls to handleScroll are throttled.
  3. Events are set up on componentDidMount and proper clean-up is done in componentWillUnmount
  4. The footprint of this component is tiny. The only properties it changes in the component it wraps are: position and bottom CSS styles.

There’s room for improvement:

  1. Browser detection to make the component a no-op in non-Safari mobile environments.
  2. Check for hidden elements on mount: maybe the element is rendered during or after the overflow bar appears.

See a sample usage below:

react-fixed-bottom in action

Final words

Surprisingly, I wasn’t able to find any readily-available solutions or analysis for the problem in our usual suspects: there weren’t any StackOverflow or packages out there in the wild, so feel free to spread the word: let your colleagues know Safari is no longer going to be the worst enemy of their fixed bottom components!

PS: there’s an npm component (react-fixed-bottom, also available in GitHub) so this can be shared with the community. Hopefully, nobody else will have to go through all of the debugging in Safari mobile.

--

--

Esaú Suárez Ramos
Turo Engineering

Following the JS way since 2014. Frontend Engineer @ Turo