Using Background-Attachment CSS To Create A Sticky IFrame Advertising Background In JavaScript
Over the weekend, while Googling around, I landed on a page that happened to have a Google advertisement embedded in the content copy. Normally, I would just skip right over the ad without giving it a second thought. But, this ad had me mesmerized: as I scrolled the page, the background-image of the advertising IFrame stayed fixed in place. The engineer in me was intrigued! But, unfortunately, I was on my iPad; so, I didn't have all web-dev tools I would normally have to perform a forensic investigation. As such, I wanted to see if I could figure out how to implement a sticky iframe background in JavaScript and CSS.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
Because of CORS (Cross-Origin Resource Sharing) security restrictions in the browser, I knew that the IFrame couldn't reach up into the parent window to listen for "scroll" events. As such, I thought that maybe the parent window was sending page meta-data down to the IFrame using window.postMessage(). I thought, perhaps, the window.pageYOffset value of the parent was being used to adjust something like the "background-position" or a "translateY()" CSS property in the advertising IFrame.
But, the window.postMessage() is an asynchronous API which means that there is a delay between when the message is emitted from the origin and when it is applied to the target. This delay, while small, was enough to create visual "jank" on background-image position. If you scrolled slowly, the window.postMessage() workflow could keep up. But, when you started to scroll fast, the background-image position started to jitter quite noticeably.
After a number of failed approaches, I was about to give-up when I came across this article on responsive DoubleClick Ads by Hansjoerg Posch (what a small world!). In the article, Posch was reversing the whole messaging workflow - instead of the parent page sending messages to the IFrame, the IFrame was sending messages to the parent. The parent was then using those messages to make content changes directly in the host page.
By reversing the message direction, it means that I could have the ad "serve up styling" for the parent page. This, in turn, would mean that I might be able to use something like the "fixed" background-attachment CSS property. Only, instead of the background image living in the advertising IFrame, it would live in an IFrame wrapper and could interact (so to speak) with the scrolling of the parent page.
To explore this reversed-messaging concept, I created a parent page that would listen for the "message" event (from the advertisement IFrame) and then apply CSS styles to the host page using .setProperty():
CAUTION: I am not checking for an expected origin value on the CORS request. Normally, you would want to do this for security; however, since I'm hosting my demos on GitHub Pages, I only have one domain to work with. In a production setting, you would absolutely want to check the origin before modifying the page!
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
Using Background-Attachment CSS To Create A Sticky IFrame Advertising Background In JavaScript
</title>
</head>
<body>
<h1>
Using Background-Attachment CSS To Create A Sticky IFrame Advertising Background In JavaScript
</h1>
<p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p>
<p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p>
<p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p>
<p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p>
<p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p>
<!-- BEGIN: Advertisement. ------------------------------------------------------ -->
<div id="ad-viewport" style="display: none ;">
<iframe id="ad" src="./ad.htm" allowtransparency="true" frameborder="0"></iframe>
</div>
<script type="text/javascript">
(function() {
var adViewport = document.querySelector( "#ad-viewport" );
var adFrame = document.querySelector( "#ad" );
// Our Ad frame is going to be HIDDEN BY DEFAULT. We're going to wait for the
// ad frame to send a message indicating how it wants to be styled in the
// current page. This will allow us to attach dynamic styles to the frame and
// the viewport.
window.addEventListener( "message", handleMessage, false );
function handleMessage( event ) {
// CAUTION: For the sake of the demo, we're NOT CHECKING event.origin.
// Since this will be hosted on GitHub, I can't have multiple domains
// to simulate a CORS REQUIREMENT.
// Add all the supplied styles for the viewport.
for ( var prop in event.data.viewport.styles ) {
adViewport.style.setProperty( prop, event.data.viewport.styles[ prop ] );
}
// Add all the supplied styles for the frame.
for ( var prop in event.data.frame.styles ) {
adFrame.style.setProperty( prop, event.data.frame.styles[ prop ] );
}
}
})();
</script>
<!-- END: Advertisement. -------------------------------------------------------- -->
<p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p>
<p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p>
<p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p>
<p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p>
<p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p><p>Copy text.</p>
</body>
</html>
As you can see, the advertisement IFrame is wrapped in a container Div. This is not an uncommon HTML situation for ads. In fact, most ad frames are heavily wrapped in several containers. In this case, I'm keeping it simple. And, when the parent page receives the "message" event, it turns around and blindly applies styles to the IFrame and its wrapper.
NOTE: The "blindly applies styles" is a reason you must check the event origin in a production context.
Now, let's look at the advertisement IFrame to see what message it's posting:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style type="text/css">
html, body {
background-color: transparent ;
height: 100vh ;
margin: 0px 0px 0px 0px ;
padding: 0px 0px 0px 0px ;
}
a.link {
bottom: 0px ;
color: #ffffff ;
display: flex ;
font-size: 26px ;
font-weight: bold ;
left: 0px ;
position: fixed ;
right: 0px ;
text-decoration: none ;
text-shadow: 0px 0px 10px #000000 ;
top: 0px ;
}
a.link:hover {
text-decoration: underline ;
}
span.link__content {
margin: auto ;
}
</style>
</head>
<body>
<a href="javascript:console.log( 'clicked' );void( 0 );" class="link">
<span class="link__content">
Much Gooses! Such Wow!
</span>
</a>
<script type="text/javascript">
// Tell the parent frame to render the Ad. As part of this operation, we're going
// to tell the parent frame HOW TO STYLE the iframe and its viewport. This will
// allow us to create some interesting effects.
window.parent.postMessage(
{
viewport: {
styles: {
"background-image": "url( './goose.jpg' )",
"background-attachment": "fixed",
"display": "block",
"height": "300px",
"position": "relative",
"width": "600px"
}
},
frame: {
styles: {
"height": "100%",
"left": "0px",
"position": "absolute",
"right": "0px",
"width": "100%"
}
}
},
"*" // We don't care about CORS for this demo (same domain).
);
</script>
</body>
</html>
As you can see, the IFrame is actually posting the "background-image" and the "background-attachment" CSS properties as part of the message. This allows the IFrame to apply the background to the parent page instead of to itself. And, since the IFrame has "allowtransparency" enabled, the content of the IFrame will become superimposed on top of the applied background of the parent page.
Now, if we load this up in the browser and scroll down, we can see that as we scroll, the background image appears "fixed" in space:
Notice that as we scroll down in the parent page, the nose on the dog in the background-image stays in the same place. This is due to the "background-attachment" CSS property value of "fixed". And, it works because the background lives on the parent page, not in the IFrame itself.
I don't know if this is the way the sticky background was actually implemented in the advertisement that I saw over the weekend. But, this approach seems to work. And, I think there is something rather fascinating about using the window.postMessage() API to communicate style decisions from the IFrame up to the parent window. If nothing else, this got me use "background-attachment: fixed" for the first time in like a decade.
Want to use code from this post? Check out the license.
Reader Comments
Just a note: the use of background-attachment: fixed doesn't work on mobile devices. It seems that the only way of implementing this is using CSS transform on the Z layer. I'm now looking into that to get this kind of effect on mobile.
@Tim,
Great point. I feeeeel like I have seen this affect on a mobile device; though, I will confirm that my approach here, as you say, does not work on mobile :( Good luck! If you ever write it down, I'd love for you to drop a link to it.