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.
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 AMPstyle="overflow-y: auto; -webkit-overflow-scrolling: touch;"><head></head><bodystyle="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:
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.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.<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 is codified in the ViewportBindingIosEmbedWrapper_.
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 AMPstyle="overflow-y: auto; -webkit-overflow-scrolling: touch;"><head></head><i-amp-html-wrapperstyle="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:
<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.scrollTop
, scrollLeft
, scrollHeight
and scrollWidth
and thus “fake” measuring elements described in the original post are no longer needed.<body>
— simple padding on the wrapper element is sufficient.However, the position:fixed
problem is still present in this solution. More on this later.
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 AMPstyle="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.
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
.
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 AMPstyle="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).
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!