Make a stylish preloader with SVG
Many sites neglect users with slow connections. Ian Culshaw explains how to use SVG library Raphaël to create a preloader that’ll hold the users' attention while pages load
This article first appeared in issue 228 of .net magazine – the world's best-selling magazine for web designers and developers.
In this tutorial I’ll show you how you can keep a user engaged with your website for long enough to load the images that set your homepage out from the rest.
It’s based on Gaya Design’s QueryLoader and makes use of Raphal to create beautiful vector shapes. Our tutorial will take large images from Flickr to show the preloading in action.
Speed matters
All websites have the potential to have a global impact, and this impact will most definitely be dependant on how long it takes your site to go from ‘0%’ to completely loaded.
We’ve all had the experience of being made to wait for a Flash preloader to load its assets. But at least at times that frustration has been lessened by an interactive, playful indicator of how much longer we have to wait. With HTML and the way it is advancing, the only way we have an indication of how far our site has downloaded is with a status bar updating via a percentage, or a spinner in the address bar.
Getting started
With this in mind, we’ll go through how to give the user this piece of eye candy – hopefully grabbing their attention just long enough to give your content the coverage it deserves.
We’ll be creating a preloader with the fantastic SVG library Raphal. We’re going to use the existing and original QueryLoader library from GayaDesign. The original script gives us a loading bar that displays across the screen and fills as the images load so the first few steps will remove the styles that comes packaged with QueryLoader so we can implement our own styles.
1. Folders
In the project folder are folders named css and js, and a demo.html file. The css folder is empty but will contain style.css. The js folder contains jQuery and Raphal; the libraries are fallbacks only used if the CDN is unresponsive.
Get the Creative Bloq Newsletter
Daily design news, reviews, how-tos and more, as picked by the editors.
2. New project
Open the demo.html file. In the body we have two <section> elements: one containing our images to be preloaded, the other containing the preloader. The images will be invisible to the user until we have finished completely loading them.
The preloader contains two elements, one of which is our loader, the other being a circle we place above the loader. Beneath this lies our JavaScript, we won't need to edit demo.html as all of our code will be handled by JavaScript, but it's always good to get an understanding of the bricks and mortar behind the wallpaper and plaster.
- <!doctype html>
- <html class="no-js" lang="en">
- <head>
- <meta charset="utf-8">
- <title></title>
- <script src="//cdnjs.cloudflare.com/ajax/libs/modernizr/2.0.6/modernizr.min.js"></script>
- <link rel="stylesheet" href="css/style.css">
- </head>
- <body>
- <section id="loading">
- <figure id="innerCircle" class="circle"></figure>
- <div id="loader"></div>
- </section>
- <section id="gallery">
- <img src="http://farm1.staticflickr.com/94/243962216_49afc8c9ba_o.jpg">
- <img src="http://farm6.staticflickr.com/5283/5361320118_d193bf5639_b.jpg">
- <img src="http://farm7.staticflickr.com/6090/6125129993_9e675f8ca0_o.jpg">
- </section>
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
- <script src="//cdnjs.cloudflare.com/ajax/libs/raphael/2.0.1/raphael-min.js"></script>
- <script>window.jQuery || document.write('<script src="js/jquery.js"><\/script>')</script>
- <script>window.Raphael || document.write('<script src="js/raphael.js"><\/script>')</script>
- <script type="text/javascript" src="js/queryloader.js"></script>
- <script defer type="text/javascript">
- QueryLoader.init();
- </script>
- </body>
- </html>
3. Adding CSS
Here we add some basic styles for our preloader and some initial visibility. I’ve chosen the colour pink as a placeholder for the preloader so we can see continual improvements and code working as we go. You can remove this at any time; just delete background: pink;.
#loading { position: absolute; width: 100%; height: 100%; background: #fff;}#loading .circle { width: 206px; height: 206px; position: absolute; left: 50%; top: 50%; margin: -103px 0 0 -103px; background: pink;} #loading #loader { width: 220px; height: 220px; position: absolute; left: 50%; top: 50%; margin: -110px 0 0 -110px; background: transparent;}
4. Preloading
Now to bring the page alive with some JavaScript. Open queryloader.js in the js folder. Here we’re adding some properties to keep them all bundled in the same object in an effort to reduce memory wastage. We will refer more to these properties later on in the tutorial.
var QueryLoader = { overlay: "", loadBar: "", preloader: "", items: new Array(), doneStatus: 0, doneNow: 0, selectorPreload: "body", logoImg: false, logoCircle: false, fakeCircle: false, ieLoadFixTime: 2000, ieTimeout: "", initialise: true, sec: 0, raph: false, init: function() {
5. Gaining height
Just inside the init function we’ll add a small jQuery resize function to adjust the height of the page to reflect if a user resizes their window at any point. Without this, if the user shrunk the page vertically you could potentially lose the preloader to the formidable fold.
...initialise: true,sec:0,raph: false,init: function() { $(window).resize(function() { $('#loading').height($(window).height()); }); $('#loading').height($(window).height()); if (navigator.userAgent.match(/MSIE (\d+(?:\.\d+)+(?:b\d*)?)/) == "MSIE 6.0,6.0") { // break if IE6 return false; } if(QueryLoad.selectorPreload == 'body') { QueryLoader.spawnLoader(); QueryLoader.getImages(QueryLoader.selectorPreload); QueryLoader.createPreloading(); } else { $(document.ready(function) { QueryLoader.spawnLoader();...
6. Streamlining
In the original QueryLoader script there’s some code that we don’t need. The code injects some HTML into the page, which we will be doing later on with Raphal – so this is unnecessary for us. You should find this around line 115 of queryloader.js.
spawnLoader: function() { if (QueryLoader.selectorPreload == "body") { var height = $(window).height(); var width = $(window).width(); var position = "fixed"; } else { var height = $(QueryLoader.selectorPreload).outerHeight(); var width = $(QueryLoader.selectorPreload).outerWidth(); var position = "absolute"; } var left = $(QueryLoader.selectorPreload).offset()['left']; var top = $(QueryLoader.selectorPreload).offset()['top']; // <<< Step 6 begin removal QueryLoader.overlay = $("<div></div>").appendTo($(QueryLoader.selectorPreload)); $(QueryLoader.overlay).addClass("QOverlay"); $(QueryLoader.overlay).css({ position: position, top: top, left: left, width: width + "px", height: height + "px" }); QueryLoader.loadBar = $("<div></div>").appendTo($(QueryLoader.overlay)); $(QueryLoader.loadBar).addClass("QLoader"); $(QueryLoader.loadBar).css({ position: "relative", top: "50%", width: "0%" }); // <<< End removal},
7. Faux finish line
Here we adapt the doneLoad function, telling the #gallery to show; technically this means setting the display property to block. We also tell jQuery to animate the opacity of the #loading screen so we fade the #loading element before removing it from the DOM.
doneLoad: function() { //prevent IE from calling the fix clearTimeout(QueryLoader.ieTimeout); //determine the height of the preloader for the effect if (QueryLoader.selectorPreload == "body") { var height = $(window).height(); } else { var height = $(QueryLoader.selectorPreload).outerHeight(); } // Step 7 add this code $('#gallery').show(); $('#loading').animate({'opacity': 0}, 1200, function() { $(this).remove(); })
8. More streamlining
Next we delete more of the code from the original loading bar, this code can be found just beneath the code entered in step 7. The original animation would fade out and remove itself where (in step 7) we told our gallery to appear and fade the loader. We will adapt more of this function toward the end of the tutorial.
doneLoad: function() { ... $('#gallery').show(); $('#loading').animate({'opacity': 0}, 1200, function() { $(this).remove(); }) // <<< Step 8 Remove this code $(QueryLoader.loadBar).animate({ height: height + "px", top: 0 }, 500, "linear", function() { $(QueryLoader.overlay).fadeOut(800); $(QueryLoader.preloader).remove(); }); // <<< End removal }
9. Incrementing process
We’re introducing a call to the updateVal function here, which will update our SVG circle. QueryLoader.doneNow is the count of images completed within the loading of the page. 105 refers to the radius of the circle and this.sec is the Raphal SVG path.
imgCallback: function() { QueryLoader.doneNow++; QueryLoader.updateVal(QueryLoader.doneNow, this.items.length, 105, this.sec); QueryLoader.animateLoader();},
10. More functionality
Here we are adding the updateVal function just beneath the imgCallback function. If the initialise parameter is true it means we have a circle to animate and proceed with the drawing. Also, if the circle is complete we have to trick the circle because we can’t do a 360-degree arc.
updateVal: function(value, total, R, hand, id) { if (QueryLoader.initialise) { if(value == total) { this.raph.clear(); // 2.1.1 - CIRCLE COMPLETION. this.fakeCircle = this.raph.circle(110,110,105).attr({colour: '', "stroke-width": 10}); } else { hand.animate({arc: [value, total, R]}, 0, ">"); } }},
11. Removal services
We are removing more of the HTML that gets injected with the original QueryLoader script. Previously this would have been injecting elements into the DOM to give a 1px high <div> animating across the screen but since were using SVG this is unnecessary.
createPreloading: function() { QueryLoader.preloader = $("<div></div>").appendTo(QueryLoader.selectorPreload); $(QueryLoader.preloader).css({ height: "0px", width: "0px", overflow: "hidden" });
12. Enter Raphal
Now for the interesting stuff: we create two SVG elements that take over the elements present in our HTML. We set some variables and attributes for the SVG to understand. The #loader is larger than the #innerCircle, this gives the illusion of the loader being a border that is loading.
createPreloading: function() { var logoC = Raphael("innerCircle", 206,206); $('#innerCircle').css('z-index', '31'); this.logoCircle = logoC.circle(103,103,103).attr({'stroke': 'rgb(125,208,163)', 'fill': 'url(wave.jpg)', "stroke-width": 0}); this.raph = Raphael("loader", 220, 220), xy = 110, R = 210, this.initialise = true, param = {stroke: "#000", "stroke-width": 10}, // Custom Attribute this.raph.customAttributes.arc = function (value, total, R) { var alpha = 360 / total * value, a = (90 - alpha) * Math.PI / 180, x = xy + R * Math.cos(a), y = xy - R * Math.sin(a), color = 'rgb(29,79,107)', path; path = [["M", xy, xy - R], ["A", R, R, 0, +(alpha > 180), 1, x, y]]; // MATRIX PATH return {path: path, stroke: color}; };...
13. Custom attributes
This is a complex maze of maths that eventually gives a matrix path for the arc to be drawn as an arc. Luckily the only key values are value, total and R (radius); these are animated by updateVal, as defined earlier. The colour is set here too, but can be adjusted as you like.
// Custom Attributethis.raph.customAttributes.arc = function (value, total, R) { var alpha = 360 / total * value, a = (90 - alpha) * Math.PI / 180, x = xy + R * Math.cos(a), y = xy - R * Math.sin(a), color = 'rgb(29,79,107)', path; if (total == value) {path = [["M", xy, xy - R], ["A", R, R, 0, +(alpha > 180), 1, x, y]]; } else { path = [["M", xy, xy - R], ["A", R, R, 0, +(alpha > 180), 1, x, y]]; } return {path: path, stroke: color};};
14. Colour attributes
This goes straight after the end of the previous customAttribute.arc function. It gets used in the updateVal function and is key to making the effect of a complete circle after images have loaded. The colour can be customised or even replaced with an image, but when customising make sure you change all values to be the same or you’ll end up with multicoloured circles!
this.raph.customAttributes.colour = function() { return {stroke: 'rgb(29,79,107)'};};
15. Sectors
Following after the previous code we define the sector here after generating the path and setting its arc (which will call our arc function from step 13). As this is at the point of only setting up our preloader when we call updateVal, we set the amount of loaded images to 0.
var length = QueryLoader.items.length; this.sec = this.raph.path().attr(param).attr({arc: [0, 60, R]});QueryLoader.updateVal(0, length, 105, this.sec, 2);
16. Animation
animateLoader gets called after each image has completed loading. We replace most of the logic so I recommend just rewriting the function. The function works out how many images are left waiting to load and if we are ahead of 99 per cent we trigger the doneLoad.
animateLoader: function() {var perc = (100 / QueryLoader.doneStatus) * QueryLoader.doneNow;var angle = (3.6 * perc);QueryLoader.updateVal(QueryLoader.doneNow, this.items.length, 105, this.sec, 2);if (perc > 99) {QueryLoader.doneLoad();}},doneLoad: function() { ...
17. Customising
Here we fade out the opacity of each circle before fading out and removing the preloading elements altogether. At this point we have a completed preloader that’ll work in browsers back to IE7; we can take it further, however, to add an extra element of personality.
...doneLoad: function {...var qLoad = this;qLoad.sec.hide();qLoad.logoCircle.stop().animate({opacity: 0}, 700);qLoad.fakeCircle.stop().animate({opacity: 0}, 700, 'easeInOut'); $('#loading').css('min-height', 'auto').animate({top: ($(window).height()*-1) + 'px'}, '800', function() { $(this).remove(); });});...
18. Shrinking elements
By making use of CSS transforms we can scale or ‘shrink’ the elements. In the current case this means the circles, in order to provide them with the appearance of charging before they fade out of the screen. We do this by wrapping the opacity animation with a 'transform' animation.
...doneLoad: function() {...var qLoad = this;qLoad.sec.hide();qLoad.logoCircle.stop().animate({transform: "s0.6 0.6 103 103"}, '1000', "easeInOut");qLoad.fakeCircle.stop().animate({transform: "s0.6 0.6 110 110"}, '1000', "easeInOut", function() {qLoad.logoCircle.stop().animate({opacity: 0}, 700);qLoad.fakeCircle.stop().animate({opacity: 0}, 700, 'easeInOut');$('#loading').css('min-height', 'auto').animate({top: ($(window).height()*-1) + 'px'}, '800', function() {$(this).remove();});});}...
19. Fly by
Adding the keyframes and animation definitions means we can apply CSS transforms through the CSS as well as the JavaScript in order to create the flying up of the screen effect we display here. Voil: we have a lovely preloader for our large image gallery!
/* Keyframes */@-webkit-keyframes fly-away { 0% { -webkit-transform: translate3d(0,0, 0); } 100% { -webkit-transform: translate3d(0,-900px, 0); }}@-moz-keyframes fly-away { 0% { -moz-transform: translate(0,0); } 100% { -moz-transform: translate(0,-900px); }}@-ms-keyframes fly-away { 0% { -ms-transform: translate(0,0); } 100% { -ms-transform: translate(0,-900px); }}...
Thank you for reading 5 articles this month* Join now for unlimited access
Enjoy your first month for just £1 / $1 / €1
*Read 5 free articles per month without a subscription
Join now for unlimited access
Try first month for just £1 / $1 / €1
The Creative Bloq team is made up of a group of design fans, and has changed and evolved since Creative Bloq began back in 2012. The current website team consists of eight full-time members of staff: Editor Georgia Coggan, Deputy Editor Rosie Hilder, Ecommerce Editor Beren Neale, Senior News Editor Daniel Piper, Editor, Digital Art and 3D Ian Dean, Tech Reviews Editor Erlingur Einarsson and Ecommerce Writer Beth Nicholls and Staff Writer Natalie Fear, as well as a roster of freelancers from around the world. The 3D World and ImagineFX magazine teams also pitch in, ensuring that content from 3D World and ImagineFX is represented on Creative Bloq.