Hackernoon logoAMP, iOS, Scrolling and Position Fixed Redo — the wrapper approach by@dvoytenko

AMP, iOS, Scrolling and Position Fixed Redo — the wrapper approach

Author profile picture

@dvoytenkoDima Voytenko

New Solution

AMP is currently experimenting with a new approach to implement scrollable iframes. It’s described in detail in AMP, iOS, Scrolling Redo 2 — the shadow wrapper approach.

The problem and the original solution

This article is a follow up on the problem and solution described in the original post AMP, iOS, Scrolling and Position Fixed.

To reminisce, AMP documents are often shown in a scrollable iframe. The structure would normally look like this:

<html>
<head>
<title>I'm a Web App and I show AMP documents</title>
<style>
iframe {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
</head>
<body>
<iframe … width="100%" height="100%"
scrolling="yes"
src="https://cdn.ampproject.org/c/pub1.com/doc1"></iframe>
</body>
</html>

This generally works great in most of browsers. We did try many other approaches, including sizing iframe by the content and scrolling the main document. But they all have significant functional detriments and performance issues. See the original post for details.

However, iOS Safari does not support scrollable iframes. In other words scrolling="yes" is simply ignored. See this demo. The long-standing iOS Safari issue can be found at bugs.webkit.org/149264.

We found an original workaround codified in the ViewportBindingNaturalIosEmbed_. In short, we scroll the actual <body> element of the document. Thus, even though the iframe itself does not scroll, the content of the iframe does.

The resulting AMP document looks like this:

<html AMP
style="overflow-y: auto; -webkit-overflow-scrolling: touch;">
<head></head>
<body
style="
overflow-y: auto;
-webkit-overflow-scrolling: touch;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
"
>
<!-- document content -->
</body>
</html>

The iframe now scrolls! This has been the solution we used in AMP for a year. However, a list of problems was adding up over time. The are described in detail in original post, but in short:

  • Applying position:absolute on the <body> is unexpected and interferes with author styles. One side effect, for instance, is we don’t allow customizing margin on the <body> element.
  • Body’s scrollTop, scrollLeft, scrollHeight and scrollWidth do not work due to bugs.webkit.org/106133. This is solved by injecting DOM with “fake” measuring elements as described in the original post.
  • position:fixed is buggy inside the -webkit-overflow-scrolling:touch container. See bugs.webkit.org/154399.
  • Offsetting headers and footers requires setting border on <body> which is expensive, reduces scrollable area and occasionally breaks existing layouts. Hiding headers causes significant jumps in UI and broken scrolling.

Can we improve on this? Enter the new solution…

The new solution — the wrapper approach

The new solution is codified in the ViewportBindingIosEmbedWrapper_.

DOM structure

At its core, the wrapper approach is similar to scrollable <body>. Iframes still don’t scroll in iOS Safari and thus we need to scroll the content of the iframe. Since scrolling <body> has a lot of problems, we could create a scrollable wrapper and place it between <html> and <body> elements. In other words, we would wrap the <body> element in the scrollable container.

So, the new DOM structure would look like this:

<html AMP
style="overflow-y: auto; -webkit-overflow-scrolling: touch;">
<head></head>
<i-amp-html-wrapper
style="
display: block;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
"
>
<body style="position: relative;">
<!-- document content -->
</body>
</i-amp-html-wrapper>
</html>

While this inarguably looks very “strange”, it does indeed solve the original problem — it makes iframes scrollable on iOS Safari. In addition this also solves many problems described above:

  • There are no special requirements forced on the <body> element: it’s normally positioned and has the default overflow:visible style. AMP allows most of CSS styles anywhere in the DOM so this reduces our interference with authors’ styling.
  • The scrollable wrapper element can be used to read scrollTop, scrollLeft, scrollHeight and scrollWidth and thus “fake” measuring elements described in the original post are no longer needed.
  • Offsetting headers and footers no longer requires setting a border on the <body> — simple padding on the wrapper element is sufficient.

However, the position:fixed problem is still present in this solution. More on this later.

Two <html> elements

We implemented the wrapper solution and quickly ran into a small problem. A lot of people like to use html > body selectors which we broke by injecting i-amp-html-wrapper between <html> and <body>. As a fix, we renamed i-amp-html-wrapper to <html>.

The final DOM structure now looks like this:

<html AMP
style="overflow-y: auto; -webkit-overflow-scrolling: touch;">
<head></head>
<html id="i-amp-html-wrapper"
style="
display: block;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
">
<body style="position: relative;">
<!-- document content -->
</body>
</html>
</html>

Double the “strange”, double the fun. But html > body CSS selectors now work correctly.

Implementation

AMP runtime creates wrapper as early as possible in the startup. The existing <body> element is simply reparented inside the wrapper:

// Create wrapper.
const wrapper = document.createElement('html');
wrapper.id = 'i-amp-html-wrapper';
// Setup classes and styles.
wrapper.className = document.documentElement.className;
document.documentElement.className = '';
document.documentElement.style = '...';
wrapper.style = '...';
// Attach wrapper straight inside the document root.
document.documentElement.appendChild(wrapper);
// Reparent the body.
const body = document.body;
wrapper.appendChild(body);
Object.defineProperty(document, 'body', {
get: () => body,
});

This code is mostly straightforward, with a small nuance — reparenting the body resets document.body to null, thus we need to override document.body property back to the original <body> element, which is done using Object.defineProperty.

Position:fixed problem

While the wrapper approach solves many issues, the position:fixed problem still remains.

This problem is described in detail in the original post. The related iOS Safari bug is bugs.webkit.org/154399.

In short, a position:fixed element inside the -webkit-overflow-scrolling:touch container jumps and flickers when scrolling. It looks like the position:fixed element is slightly scrolled and then quickly jumps back into its correct place. See this video for demo.

In our original solution, we move qualifying position:fixed elements out of the original <body> and into the synthetic “fixed layer” element that’s placed in DOM outside the -webkit-overflow-scrolling:touch container.

The final DOM structure looks like this:

<html AMP
style="overflow-y: auto; -webkit-overflow-scrolling: touch;">
<head></head>
<html id="i-amp-html-wrapper"
style="
display: block;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
">
<body style="position: relative;">
<!-- document content -->
</body>
</html>
  <body id="i-amp-fixed-layer"
style="
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
"
>
<!-- fixed elements reparented here -->
</body>
</html>

Thus, we end up with two <html> elements and two <body> elements. It looks completely crazy, but it does solve the two original problems: iframes now scroll and position:fixed elements do not flicker.

Obviously, we’d be much nicer if the underlying iOS Safari issue were fixed (bugs.webkit.org/149264).

References

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising &sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.