Buttons With Built-in Progress Meters

Buttons With Built-in Progress Meters

Progress bars have become quite popular lately, and there are lots of plugins that can help you add one to your site. But how do you make one yourself? The problem is that there are plenty of implementations already, so in this tutorial, we are going to code something different – buttons that have built in progress meters.

They will be perfect for displaying progress while submitting forms or loading content via AJAX. They will also use CSS3 styles and transitions to make them easy to customize.

The HTML

In the first section of the tutorial, we will write the HTML markup. It comes in the form of a standard HTML5 document which includes two additional resources that we will discuss later – the styles.css stylesheet and the script.js JavaScript file. Additionally, I have included the jQuery library, and the Raleway font from Google web fonts.

index.html

<!DOCTYPE html>
<html>

	<head>
		<meta charset="utf-8"/>
		<title>Tutorial: Buttons with built-in progress meters</title>

		<link href="http://fonts.googleapis.com/css?family=Raleway:400,700" rel="stylesheet" />

		<!-- The Stylesheets -->
		<link href="assets/css/style.css" rel="stylesheet" />

	</head>

	<body>

		<h1>Progress Buttons</h1>

		<a id="submitButton" href="#" class="progress-button">Submit</a>

		<a id="actionButton" href="#" class="progress-button green" data-loading="Working.." data-finished="Finished!" data-type="background-bar">Action!</a>

		<a id="generateButton" href="#" class="progress-button red" data-loading="Generating.." data-finished="Download" data-type="background-vertical">Generate</a>

		<h1>Progress Control</h1>

		<a id="controlButton" href="#" class="progress-button">Start</a>

		<div class="control-area">
			<a class="command increment">Increment</a>
			<a class="command set-to-1">Set to 1%</a>
			<a class="command set-to-50">Set to 50%</a>
			<a class="command finish">Finish</a>
		</div>

		<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
		<script src="assets/js/script.js"></script>

	</body>
</html>

The markup is pretty simple. The progress buttons are defined as regular hyperlinks. In order to be recognized by the plugin and turned into buttons with built-in progress bars, they need to have the .progress-button class. Buttons can also be configured by setting three data-* attributes:

  • data-type specifies what type of progress bars will be shown. Currently three types are supported: background-horizontal (the default), background-bar and background-vertical.
  • data-loading specifies the text that is shown while the progress bar is moving. The default value is Loading..
  • data-finished holds the text that is set on the button when the progress is done. The default value is Done!

If you omit an attribute, the default value will be used.

Buttons With Built-in Progress Meters

Buttons With Built-in Progress Meters

The jQuery Code

In this part of the tutorial, we are going to write the JavaScript and jQuery code to make the buttons work. The code is organized as 6 jQuery plugins that share a common name – progressInitialize, progressStart, progressIncrement, progressTimed, progressSet and progressFinish. I have provided plenty of comments in the code, so you can dig right in:

assets/js/script.js

$(document).ready(function(){

	// Convert all the links with the progress-button class to
	// actual buttons with progress meters.
	// You need to call this function once the page is loaded.
	// If you add buttons later, you will need to call the function only for them.

	$('.progress-button').progressInitialize();

	// Listen for clicks on the first three buttons, and start 
	// the progress animations

	$('#submitButton').click(function(e){
		e.preventDefault();

		// This function will show a progress meter for
		// the specified amount of time

		$(this).progressTimed(2);
	});

	$('#actionButton').click(function(e){
		e.preventDefault();
		$(this).progressTimed(2);
	});

	$('#generateButton').one('click', function(e){
		e.preventDefault();

		// It can take a callback

		var button = $(this);
		button.progressTimed(3, function(){

			// In this callback, you can set the href attribute of the button
			// to the URL of the generated file. For the demo, we will only 
			// set up a new event listener that alerts a message.

			button.click(function(){
				alert('Showing how a callback works!');
			});
		});
	});

	// Custom progress handling

	var controlButton = $('#controlButton');

	controlButton.click(function(e){
		e.preventDefault();

		// You can optionally call the progressStart function.
		// It will simulate activity every 2 seconds if the
		// progress meter has not been incremented.

		controlButton.progressStart();
	});

	$('.command.increment').click(function(){

		// Increment the progress bar with 10%. Pass a number
		// as an argument to increment with a different amount.

		controlButton.progressIncrement();
	});

	$('.command.set-to-1').click(function(){

		// Set the progress meter to the specified percentage

		controlButton.progressSet(1);
	});

	$('.command.set-to-50').click(function(){
		controlButton.progressSet(50);
	});

	$('.command.finish').click(function(){

		// Set the progress meter to 100% and show the done text.
		controlButton.progressFinish();
	});

});

// The progress meter functionality is available as a series of plugins.
// You can put this code in a separate file if you wish to keep things tidy.

(function($){

	// Creating a number of jQuery plugins that you can use to
	// initialize and control the progress meters.

	$.fn.progressInitialize = function(){

		// This function creates the necessary markup for the progress meter
		// and sets up a few event listeners.

		// Loop through all the buttons:

		return this.each(function(){

			var button = $(this),
				progress = 0;

			// Extract the data attributes into the options object.
			// If they are missing, they will receive default values.

			var options = $.extend({
				type:'background-horizontal',
				loading: 'Loading..',
				finished: 'Done!'
			}, button.data());

			// Add the data attributes if they are missing from the element.
			// They are used by our CSS code to show the messages
			button.attr({'data-loading': options.loading, 'data-finished': options.finished});

			// Add the needed markup for the progress bar to the button
			var bar = $('<span class="tz-bar ' + options.type + '">').appendTo(button);

			// The progress event tells the button to update the progress bar
			button.on('progress', function(e, val, absolute, finish){

				if(!button.hasClass('in-progress')){

					// This is the first progress event for the button (or the
					// first after it has finished in a previous run). Re-initialize
					// the progress and remove some classes that may be left.

					bar.show();
					progress = 0;
					button.removeClass('finished').addClass('in-progress')
				}

				// val, absolute and finish are event data passed by the progressIncrement
				// and progressSet methods that you can see near the end of this file.

				if(absolute){
					progress = val;
				}
				else{
					progress += val;
				}

				if(progress >= 100){
					progress = 100;
				}

				if(finish){

					button.removeClass('in-progress').addClass('finished');

					bar.delay(500).fadeOut(function(){

						// Trigger the custom progress-finish event
						button.trigger('progress-finish');
						setProgress(0);
					});

				}

				setProgress(progress);
			});

			function setProgress(percentage){
				bar.filter('.background-horizontal,.background-bar').width(percentage+'%');
				bar.filter('.background-vertical').height(percentage+'%');
			}

		});

	};

	// progressStart simulates activity on the progress meter. Call it first,
	// if the progress is going to take a long time to finish.

	$.fn.progressStart = function(){

		var button = this.first(),
			last_progress = new Date().getTime();

		if(button.hasClass('in-progress')){
			// Don't start it a second time!
			return this;
		}

		button.on('progress', function(){
			last_progress = new Date().getTime();
		});

		// Every half a second check whether the progress 
		// has been incremented in the last two seconds

		var interval = window.setInterval(function(){

			if( new Date().getTime() > 2000+last_progress){

				// There has been no activity for two seconds. Increment the progress
				// bar a little bit to show that something is happening

				button.progressIncrement(5);
			}

		}, 500);

		button.on('progress-finish',function(){
			window.clearInterval(interval);
		});

		return button.progressIncrement(10);
	};

	$.fn.progressFinish = function(){
		return this.first().progressSet(100);
	};

	$.fn.progressIncrement = function(val){

		val = val || 10;

		var button = this.first();

		button.trigger('progress',[val])

		return this;
	};

	$.fn.progressSet = function(val){
		val = val || 10;

		var finish = false;
		if(val >= 100){
			finish = true;
		}

		return this.first().trigger('progress',[val, true, finish]);
	};

	// This function creates a progress meter that 
	// finishes in a specified amount of time.

	$.fn.progressTimed = function(seconds, cb){

		var button = this.first(),
			bar = button.find('.tz-bar');

		if(button.is('.in-progress')){
			return this;
		}

		// Set a transition declaration for the duration of the meter.
		// CSS will do the job of animating the progress bar for us.

		bar.css('transition', seconds+'s linear');
		button.progressSet(99);

		window.setTimeout(function(){
			bar.css('transition','');
			button.progressFinish();

			if($.isFunction(cb)){
				cb();
			}

		}, seconds*1000);
	};

})(jQuery);

progressInitialize sets up an event listener for the progress custom event that the other functions call whenever the meter needs to be updated. Thanks to the custom event, we can have entirely independent functions like progressStart, which manages its own timer and state – progresInitialize need not know about progressStart.

The other important thing is that we are setting two special classes on the buttons – .in-progress while the progress meter is moving, and .finished when it’s ready. They are used to update the text of the buttons as you will see in the next section.

The CSS

I mentioned that we are setting two CSS classes on the buttons – .in-progress and .finished. But how does adding one of these classes change the text of the button? Simple – we are using a CSS trick that involves the CSS3 attr operator, which when combined with content, can set the text of a :before or :after pseudo element to that of the attribute of the element. It will become clearer once you see it for yourself (lines 44-52):

assets/css/styles.css

.progress-button{
	display: inline-block;
	font-size:24px;
	color:#fff !important;
	text-decoration: none !important;
	padding:14px 60px;
	line-height:1;
	overflow: hidden;
	position:relative;

	box-shadow:0 1px 1px #ccc;
	border-radius:2px;

	background-color: #51b7e6;
	background-image:-webkit-linear-gradient(top, #51b7e6, #4dafdd);
	background-image:-moz-linear-gradient(top, #51b7e6, #4dafdd);
	background-image:linear-gradient(top, #51b7e6, #4dafdd);
}

/* Hide the original text of the button. Then the loading or finished
   text will be shown in the :after element above it. */

.progress-button.in-progress,
.progress-button.finished{
	color:transparent !important;
}

.progress-button.in-progress:after,
.progress-button.finished:after{
	position: absolute;
	z-index: 2;
	width: 100%;
	height: 100%;
	text-align: center;
	top: 0;
	padding-top: inherit;
	color: #fff !important;
	left: 0;
}

/* If the .in-progress class is set on the button, show the
   contents of the data-loading attribute on the butotn */

.progress-button.in-progress:after{
	content:attr(data-loading);
}

/* The same goes for the .finished class */

.progress-button.finished:after{
	content:attr(data-finished);
}

/* The colorful bar that grows depending on the progress */

.progress-button .tz-bar{
	background-color:#e667c0;
	height:3px;
	bottom:0;
	left:0;
	width:0;
	position:absolute;
	z-index:1;

	border-radius:0 0 2px 2px;

	-webkit-transition: width 0.5s, height 0.5s;
	-moz-transition: width 0.5s, height 0.5s;
	transition: width 0.5s, height 0.5s;
}

/* The bar can be either horizontal, or vertical */

.progress-button .tz-bar.background-horizontal{
	height:100%;
	border-radius:2px;
}

.progress-button .tz-bar.background-vertical{
	height:0;
	top:0;
	width:100%;
	border-radius:2px;
}

The rest of the code styles the buttons and the built-in progress meter. In styles.css I have also included two additional color themes and some other rules that are not given here, but which you can see by browsing the source yourself.

We’re Done!

The code we wrote today is ready for customizations. Simply open styles.css in your favorite code editor and change the colors, fonts or styles so they match your site. Change the text by editing the HTML and the data attributes. Or you can even improve the code with some cool new feature. In this case be sure to share it in the comment section :)

Join our newsletter and get our PSDs!18,821 people learn about HTML5, JS and more. Join them!

by Martin Angelov

Martin is a web developer with an eye for design from Bulgaria. He founded Tutorialzine in 2009 and it still is his favorite side project.

16 Comments

  1. Stavro says:

    Hey

    This Tutorial is awesome but ive a question.

    How can i do it that the buttons for eg. work as loading button for a new page.
    I mean that they show than how much the next page have load and then (if the button reach 100%) the new page appears.

    Thanks

    1. Martin Angelov says:

      You could do this with AJAX - you will fetch the HTML of the next page and show it. However it will be best to use a library like jQuery-pjax.

  2. Without reading further I thought, it (somehow) tracks the progress of the underlying (ajax) call and shows the progress.

    Essentially we ourselves give the time duration it takes to animate the full button, yeah?

    We can use jquery:ajaxSuccess to push the progress from say, at 37% to a 100% whenever the event successfully returns, no?

    1. Martin Angelov says:

      Yes, this is the idea - you can use the methods that the code exposes to drive the progress meter yourself. Some AJAX events do return the progress, which you can use for an accurate percentage.

  3. abdullah says:

    Thank you very much! Beautiful tutorial!

  4. Kyle says:

    Found a typo:

    $('#generateButton').one('click', function(e)

    Otherwise, great tutorial! Love the idea.

    1. Martin Angelov says:

      It isn't a typo - there really is a "one" method. I am using it so that the click handler is executed only once.

  5. Neil B says:

    Something that ladda has been doing for a while now

    1. Martin Angelov says:

      Great find! I don't claim that I've invented this concept - I am teaching people how to make it.

  6. John says:

    Really interesting idea, thank you for the code!

  7. Nicolas M. says:

    Always excellents tutorials !

    Before I was using the font-awesome spinner's icon, now I will use this progress meter :)
    Very nice design

    Thanks Martin

  8. Alex says:

    Firstly, thank you very much, great tutorial and code!

    Secondly, how would you then implement this into a real loading enviroment, for example if you were loading an image in with ajax, where would the handling go?

    Thank you

  9. Anshul says:

    Why don't you host your code on github so that people can contribute.

    1. Martin Angelov says:

      As sad as it sounds, I have no time to support another open source project. If you wish to lead the project, you can take the code, push it on github and I will link to it from the article.

  10. Ashik says:

    This is a nice tutorial Martin. Can you please show me how to use ajaxSuccess() to push the progress based on the content load? It would be very helpful for my project.

    Thank you.

Add Comment

Add a Reply

HTML is escaped automatically. Surround code blocks with <pre></pre> for readability.
Perks:   **bold**   __italics__   [some text](http://example.com) for links