Photograph of Janelle Monáe in concert, shot in silhouette against a blue light

Of late has featured some interesting and innovative web design techniques. One of the most notable is a “Ken Burns” technique employed on header images: a subtle combination of zoom and pan that is both engaging and unpredictable, choosing random locations in the image to slowly focus on over time.

A few of my students have been asking how they might achieve this in their own work. While I haven’t looked at the code on the AskMen site, I’m happy to share how I would pull it off, using a little JavaScript to generate a random CSS animation.

First, we need to set up the image:

<figure id="burnsbox">
	<img src="janelle-monae.jpg" alt="Photograph of Janelle Monae in concert, shot in silhouette against a blue light">

The CSS use a variation of the technique I’ve covered recently: a relatively-positioned box containing absolutely-positioned responsive content.

figure#burnsbox {
	overflow: hidden;
	position: relative;
	padding-top: 60%;
figure#burnsbox img {
	position: absolute; 
	top: 0; left:-5%;
	width: 110%;
	height: 110%;

Note that the image is made slightly larger that its container, overlapping by 5% on each side: Illustration showing an image larger than its container

This overlap (shown in orange) is hidden by the overflow: hidden on the container (shown as a black border). The padding-bottom on the figure element preserves the correct height by using a value that matches the aspect ratio of the image.

Setting Expectations

To make the effect work we need to do several things:

  • Generate a random vector and scale for the image with JavaScript. These measurements must be within limits that won’t reveal the edges of the image.
  • Create a CSS keyframe animation that utilizes these values.
  • Determine if the animation and transforms require vendor prefixes.
  • Write the animation into the page’s existing embedded stylesheet.

For this example, let’s assume the following:

  • The final scale of the image will be 1.1 to 1.4× its original size.
  • The animation will always take 12 seconds to complete
  • The image can move no less than than 5% of its current width and height in any direction, but no more than 10%.

Creating A Random CSS Animation

Under those conditions, generating the initial random numbers is easy:

function randomizer(min,max) {
	randomresult = Math.random() * (max - min) + min;
	return randomresult;
var maxscale = 1.4,
minscale = 1.1,
minMov = 5,
maxMov = 10,
scalar = randomizer(minscale,maxscale).toFixed(2),
moveX = randomizer(minMov,maxMov).toFixed(2);
moveX = Math.random() < 0.5 ? -Math.abs(moveX) : Math.abs(moveX);
var moveY = randomizer(minMov,maxMov).toFixed(2);
moveY = Math.random() < 0.5 ? -Math.abs(moveY) : Math.abs(moveY);

I’ve used toFixed just to avoid the unnecessary complication of trying to scale to a number like 1.269274802; the line of code after the determination of the value of moveX and moveY randomly switches the result between positive and negative.

Next, we need to look for an existing embedded style sheet in the document:

var lastSheet = document.styleSheets[document.styleSheets.length - 1];

Since we don’t want to write out the animation code more than once, we’ll need to determine if the browser requires prefixes. In this case, I’ll use the CSSOM:

var prefix = "";
	prefix = "-webkit-";
	prefix = "-moz-"; 

Finally, we need to create the animation sequence, which will pan and zoom the image for 80% of the animation time, holding it at both the start and beginning for 10% of the total duration. The animation will be added to the existing stylesheet using insertRule, placing it at the very start of the CSS (i.e. index position 0).

lastSheet.insertRule("@"+prefix+"keyframes zoomzoom {" + 
"10% { " + prefix + "transform: scale(1); } " + 
"90% { " + prefix + "transform: scale(" + scalar +" ) translate(" + moveX +"%," +  moveY + "%); } " +
"100% { " + prefix + "transform: scale(" + scalar + ") translate(" + moveX +"%," +  moveY +"%); } }", 0);

(I’ve added more concatenation than absolutely necessary to break up the lines for increased readability).

If the script was used in an up-to-date version of Firefox, the randomized result might look something like this:

keyframes zoomzoom {
	10% {
		transform: scale(1);
	90% {
		transform: scale(1.34) translate(7.21%, 5.89%);
	100% {
		transform: scale(1.34) translate(7.21%, 5.89%);

The final step is to control the image with the generated keyframe animation sequence. We could do that with more JavaScript, or just by adding the declarations to the existing styles:

figure#burnsbox img {
	position: absolute;
	top: 0; left:-5%; 
	width: 110%;
	height: 110%;
	animation: zoomzoom 12s linear alternate infinite;

An alternative and slightly safer approach is to create the new stylesheet only when the page has completed loading, pushing the appropriate element towards the newly-formed animation. The rest of the code remains unchanged:

window.onload = function(){
	sheet = document.createElement('style');
	var anim = "@"+prefix+"keyframes burnseffect { 
		10% { " + prefix + "transform: scale(1); }
		90% { " + prefix + "transform: scale(" + scalar +" ) translate(" + moveX +"%," +  moveY + "%); }
		100% { " + prefix + "transform: scale(" + scalar + ") translate(" + moveX +"%," +  moveY +"%); } 
		var monae = document.querySelector("figure#burnsbox img"); = 'burnseffect'; = 'burnseffect'; = 'burnseffect';

Of course you can also control any other aspect of CSS animation with JavaScript, including duration and behaviour, all of which I will cover in future blog articles.

The result is what you can see above: a randomized, repeating pan-and-zoom sequence for the photograph of Janelle Monáe in concert, taken by Nastassia Davis and licensed under Creative Commons.

Photograph of Janelle Monáe in concert, taken by Nastassia Davis and licensed under Creative Commons.

Enjoy this piece? I invite you to follow me at to learn more.
Check out the CodePen demo for this article at