How to Create a Circular Progress Button

A tutorial on how to implement the circular progress button concept by Colin Garven. We are using the SVG line drawing animation technique as described by Jake Archibald to animate the circular progress and provide a success and error state to indicate the final status.

AnimatedProgressButton

View demo Download source

Today we are going to show you how to implement a nifty progress button concept. The concept is the fabulous Submit Button by Colin Garven. Take a look at it first to get an idea of what steps need to be done, and enjoy the animation. The idea behind the button is the following: once clicked, the submit button is transformed into a circle that will show a progress animation using its border. When the progress animation is finished, the button will expand again and show a checkmark to confirm that the submission is complete, as Colin mentions in a comment. We are going to implement this concept and add another state for the case when the submission fails.

There are quite some possibilities for creating this button and the effect. When thinking about CSS-only techniques, the most challenging part would be the progress circle. There is a smart technique using the clip property for achieving the effect and Anders Ingemann wrote an excellent and very complete tutorial on it (he uses LESS). But we are going to implement this beauty using an SVG based technique with CSS transitions and some JavaScript. For the progress circle, the checkmark and cross in particular, we’ll make use of the animated line drawing technique explained by Jake Archibald.

Note that animating SVGs can be problematic for some browsers so it might not work everywhere as expected. These kind of techniques are still in their infancy so consider this tutorial as an experimental exercise that might come in handy for future implementations. ;)

So, let’s get started!

The Master plan

If you have carefully observed Colin’s Dribbble shot, you might have noticed that we’ll need to take care of several states of the button. The interesting part is the transition from one state to another.
First, we want to show a simple button with a transparent background and a colored border. When we hover over the button, we want it to get filled with the border color and the text should become white.
AnimatedProgressButton01_hover

When we click on the button (in order to, for example, submit a form), we want to fade out the text, decrease the button’s width to become a circle, make the border thicker and start a progress animation on the border. We are going to use an SVG circle for this animation, so we need to ensure that the decreased round button is of the same size and sits in the same position as the SVG circle, that we’ll show in that moment.
We’ll then draw the circle stroke, simulating the progress of the submission.
AnimatedProgressButton02_progress

Once the submission is complete, i.e. the stroke is all drawn, we have to make our button expand again and draw the checkmark in case of a successful submission. We’ll also color the button accordingly.
AnimatedProgressButton03_success

For the case of a failed submission, we’ll also want an error state style.
AnimatedProgressButton04_error

So, let’s create our markup with all the elements that we need.

The Markup

For our markup we need a main container, a button with a span that contains the text, and the three SVGs:

<!-- progress button -->
<div id="progress-button" class="progress-button">
	<!-- button with text -->
	<button><span>Submit</span></button>

	<!-- svg circle for progress indication -->
	<svg class="progress-circle" width="70" height="70">
		<path d="m35,2.5c17.955803,0 32.5,14.544199 32.5,32.5c0,17.955803 -14.544197,32.5 -32.5,32.5c-17.955803,0 -32.5,-14.544197 -32.5,-32.5c0,-17.955801 14.544197,-32.5 32.5,-32.5z"/>
	</svg>

	<!-- checkmark to show on success -->
	<svg class="checkmark" width="70" height="70">
		<path d="m31.5,46.5l15.3,-23.2"/>
		<path d="m31.5,46.5l-8.5,-7.1"/>
	</svg>

	<!-- cross to show on error -->
	<svg class="cross" width="70" height="70">
		<path d="m35,35l-9.3,-9.3"/>
		<path d="m35,35l9.3,9.3"/>
		<path d="m35,35l-9.3,9.3"/>
		<path d="m35,35l9.3,-9.3"/>
	</svg>

</div><!-- /progress-button -->

We’ve used Method Draw, an easy-to-use online SVG editor to draw the checkmark and cross previously. The size for all SVGs will be 70×70 because our button has a height of 70 pixels. Since we want the circle to have a stroke thickness of 5 pixel in order to look like in Colin’s concept, we need to set the correct radius when we draw it in the graphics editor so that the whole circle together with its stroke fills the button height of 70 pixels. Note that strokes in SVG are drawn half inset and half outset. For example, a stroke of 2 will increase a circle of radius 10 to a “real” width and height of 20 plus 2 instead of 20 plus 4 (two times the border width), so the formula is 2r + border. So, in our case we know that 2r + 5 = 70, hence our circle needs a radius of 32.5 and we end up with this shape <circle cx="35" cy="35" r="32.5"/>.

Unfortunately, we can’t just use this basic shape because the starting point of the “path” will differ in browsers and so we can’t control where the “progress animation” starts. So, we’ll convert this circle shape to a path and use that instead (see above). You can easily do that in Method Draw under Object > Convert to Path.

For the cross we will use four paths so that we can draw them from a middle point, making it look similar to the checkmark animation.

So, now we have all the elements that we need. Let’s think about the action flow and start styling!

The CSS

First, we need to style our button container. It’s like the outer skin of our button, so let’s make it a bit more like a button and set it’s display to inline-block so that we can use it in the flow:

.progress-button {
	position: relative;
	display: inline-block;
	text-align: center;
}

Our button needs some coloring and typographic love. In order to make it look like Colin’s button, we set the right border and choose Montserrat as font:

.progress-button button {
	display: block;
	margin: 0 auto;
	padding: 0;
	width: 250px;
	height: 70px;
	border: 2px solid #1ECD97;
	border-radius: 40px;
	background: transparent;
	color: #1ECD97;
	letter-spacing: 1px;
	font-size: 18px;
	font-family: 'Montserrat', sans-serif;
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s, border-width 0.3s, border-color 0.3s;
	transition: background-color 0.3s, color 0.3s, width 0.3s, border-width 0.3s, border-color 0.3s;
}

We also need to set a transition for all those properties that we’ll be animating, like the background-color, the width and so on.

On hover, we’ll change the background color and the color:

.progress-button button:hover {
	background-color: #1ECD97;
	color: #fff;
}

Let’s remove any shiny outlines:

.progress-button button:focus {
	outline: none;
}

All SVGs should be positioned absolutely in the center and we’ll not allow any pointer-events:

.progress-button svg {
	position: absolute;
	top: 0;
	left: 50%;
	-webkit-transform: translateX(-50%);
	transform: translateX(-50%);
	pointer-events: none;
}

The paths should not have any fill as we only want to play with the strokes. We don’t want to show them in any state except their special state, so let’s hide them by making them transparent:

.progress-button svg path {
	opacity: 0;
	fill: none;
}

Our progress ring will be created by setting the stroke of our circle path to 5:

.progress-button svg.progress-circle path {
	stroke: #1ECD97;
	stroke-width: 5;
}

The success/error indicators will have a thinner stroke and they’ll be white. We’ll also set the linecap of the stroke to round, so that they look nicer. These two will have a quick opacity transition:

.progress-button svg.checkmark path,
.progress-button svg.cross path {
	stroke: #fff;
	stroke-linecap: round;
	stroke-width: 4;
	-webkit-transition: opacity 0.1s;
	transition: opacity 0.1s;
}

So, let’s just recap for a moment and remember our master plan. We need to be able to “style” three additional states (besides the default state) of the button and it’s special elements: the loading state, the success and the error state. So, we will use the classes “loading”, “success” and “error” to indicate them.

The button will become a circle and look exactly like the progress circle when we start with the loading process:

.loading.progress-button button {
	width: 70px; /* make a circle */
	border-width: 5px;
	border-color: #ddd;
	background-color: transparent;
	color: #fff;
}

Remember that we’ve already set the transition when defining the button styles.

The text should fade out quickly when we start the progress animation…

.loading.progress-button span {
	-webkit-transition: opacity 0.15s;
	transition: opacity 0.15s;
}

…by setting the opacity to 0:

.loading.progress-button span,
.success.progress-button span,
.error.progress-button span {
	opacity: 0; /* keep it hidden in all states */
}

When we change from the loading to the success or error state, we don’t need the transition, we simply want to keep it hidden.

When we remove all classes and return to the default state, we want to take a bit more time for the span to appear. So we have to define a different transition duration and delay for the normal state:

/* Transition for when returning to default state */
.progress-button button span {
	-webkit-transition: opacity 0.3s 0.1s;
	transition: opacity 0.3s 0.1s;
}

When we reach the last state and the submission was either successful or there was an error, we need to redefine the transitions for our button since we don’t need the border color or width to animate:

.success.progress-button button,
.error.progress-button button {
	-webkit-transition: background-color 0.3s, width 0.3s, border-width 0.3s;
	transition: background-color 0.3s, width 0.3s, border-width 0.3s;
}

Let set the colors for the final states:

.success.progress-button button {
	border-color: #1ECD97;
	background-color: #1ECD97;
}

.error.progress-button button {
	border-color: #FB797E;
	background-color: #FB797E;
}

When we apply the respective classes, we will show the paths of our SVGs and animate the stroke-dashoffset by setting the following transition:

.loading.progress-button svg.progress-circle path,
.success.progress-button svg.checkmark path,
.error.progress-button svg.cross path {
	opacity: 1;
	-webkit-transition: stroke-dashoffset 0.3s;
	transition: stroke-dashoffset 0.3s;
}

Let’s add some optional easing for the button width animation by defining a the style for this extra class:

.elastic.progress-button button {
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.25, 0.25, 0.4, 1), border-width 0.3s, border-color 0.3s;
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.25, 0.25, 0.4, 1.6), border-width 0.3s, border-color 0.3s;
	transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.25, 0.25, 0.4, 1.6), border-width 0.3s, border-color 0.3s;
}

.loading.elastic.progress-button button {
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.6, 0, 0.75, 0.75), border-width 0.3s, border-color 0.3s;
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.6, -0.6, 0.75, 0.75), border-width 0.3s, border-color 0.3s;
	transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.6, -0.6, 0.75, 0.75), border-width 0.3s, border-color 0.3s;
}

If you’d like to play with some other easing functions, check out Ceaser, the CSS Easing Animation Tool by Matthew Lein.

And that’s the style, let’s go on and do the magic!

The JavaScript

We will start by initializing/caching some elements: button is the HTML button element, progressEl is the SVG element that will have the path that represents the ring shaped progress bar, and the successEl, errorEl are the SVG elements that have the paths for the checkmark and the cross, respectively.

function UIProgressButton( el, options ) {
	this.el = el;
	this.options = extend( {}, this.options );
	extend( this.options, options );
	this._init();
}

UIProgressButton.prototype._init = function() {
	this.button = this.el.querySelector( 'button' );
	this.progressEl = new SVGEl( this.el.querySelector( 'svg.progress-circle' ) );
	this.successEl = new SVGEl( this.el.querySelector( 'svg.checkmark' ) );
	this.errorEl = new SVGEl( this.el.querySelector( 'svg.cross' ) );
	// init events
	this._initEvents();
	// enable button
	this._enable();
}

We’ll add a function SVGEl that will be used to represent an SVG element and its paths. For each one we will cache the paths and the respective lengths. We initially “undraw” all the paths by manipulating both, the strokeDasharray and the strokeDashoffset values. Later on we will “draw” them back when we want to show the progress path and the checkmark or cross paths. This technique is very well explained in Jake Archibald’s article Animated line drawing in SVG. We basically set the stroke-dasharray to the length of the path and “pull it back” so that we don’t see it anymore by setting the stroke-dashoffset to its length, too. When we want to “draw” the stroke, we will push the offset back to 0, simulating the drawing of the path.

function SVGEl( el ) {
	this.el = el;
	// the path elements
	this.paths = [].slice.call( this.el.querySelectorAll( 'path' ) );
	// we will save both paths and its lengths in arrays
	this.pathsArr = new Array();
	this.lengthsArr = new Array();
	this._init();
}

SVGEl.prototype._init = function() {
	var self = this;
	this.paths.forEach( function( path, i ) {
		self.pathsArr[i] = path;
		path.style.strokeDasharray = self.lengthsArr[i] = path.getTotalLength();
	} );
	// undraw stroke
	this.draw(0);
}

// val in [0,1] : 0 - no stroke is visible, 1 - stroke is visible
SVGEl.prototype.draw = function( val ) {
	for( var i = 0, len = this.pathsArr.length; i < len; ++i ){
		this.pathsArr[ i ].style.strokeDashoffset = this.lengthsArr[ i ] * ( 1 - val );
	}
}

Next, we need to bind the onclick event to the button. The button will initially animate to a rounded shape (by adding the class loading). After this animation ends, either an existing callback function is called (if any was specified in the options) or we simply set the progress to 100% (the speed of this “dummy” animation will be the same as defined for the transition of the stroke-dashoffset in the CSS). The button will also become disabled at this point. (This should actually be the first thing to happen when clicking it, however, if we do so Firefox does not seem to fire the transitionend event.)

UIProgressButton.prototype._initEvents = function() {
	var self = this;
	this.button.addEventListener( 'click', function() { self._submit(); } );
}

UIProgressButton.prototype._submit = function() {
	classie.addClass( this.el, 'loading' );
	
	var self = this,
		onEndBtnTransitionFn = function( ev ) {
			if( support.transitions ) {
				this.removeEventListener( transEndEventName, onEndBtnTransitionFn );
			}
			
			this.setAttribute( 'disabled', '' );

			if( typeof self.options.callback === 'function' ) {
				self.options.callback( self );
			}
			else {
				self.setProgress(1);
				self.stop();
			}
		};

	if( support.transitions ) {
		this.button.addEventListener( transEndEventName, onEndBtnTransitionFn );
	}
	else {
		onEndBtnTransitionFn();
	}
}

Once the progress reaches 100% we need to reset the stroke of the progress circle path. Also, we will either show the success checkmark or the error cross paths. After some time (options.statusTime) we “undraw” any status indicator path and enable the button again. Note that, as shown before, we control the transitions via CSS.

UIProgressButton.prototype.stop = function( status ) {
	var self = this,
		endLoading = function() {
			self.progressEl.draw(0);
			
			if( typeof status === 'number' ) {
				var statusClass = status >= 0 ? 'success' : 'error',
					statusEl = status >=0 ? self.successEl : self.errorEl;

				statusEl.draw( 1 );
				// add respective class to the element
				classie.addClass( self.el, statusClass );
				// after options.statusTime remove status and undraw the respective stroke and enable the button
				setTimeout( function() {
					classie.remove( self.el, statusClass );
					statusEl.draw(0);
					self._enable();
				}, self.options.statusTime );
			}
			else {
				self._enable();
			}

			classie.removeClass( self.el, 'loading' );
		};

	// give it a little time (ideally the same like the transition time) so that the last progress increment animation is still visible.
	setTimeout( endLoading, 300 );
}

And the button is done!

We hope you enjoyed this tutorial and find it useful!

View demo Download source

Previous:
Next:

Tagged with:

Mary Lou (Manoela Ilic) is a freelance web designer and developer with a passion for interaction design. She studied Cognitive Science and Computational Logic and has a weakness for the smell of freshly ground peppercorns.

View all contributions by

Website: http://tympanus.net/

Related Articles

Feedback 30

  1. 3

    Hello There This Website is Totally amazing and So useful……..I think not only think but provide you 10 out of 10 stars…Best Tutorials Site…keep it Up!!!

  2. 6

    I wanna know hot to redirect it to a new windows, i have tried by using a href but it dowsn’t work, it does not allow me to see the animation

  3. 7

    Hello !

    I love this example. I am using it for my web-mobile-application.

    I bound the loading animation to an AJAX query. And sometimes it takes too much time to have and the progress animation is stuck at 100%.

    So, for the user, It appears that something goes wrong

    I would like this button is loading for ever and ever untill I stop.

    Do you have any clue to give to me ?
    Is there CSS tips that allow just a part of the circle border (green) to turn for over ?
    Or have you best practices ?

    Thank you very much again for this great work !
    I love your site !

    Regards

    Corentin

  4. 9

    Newbie here but love this.
    The button is too big for my design.
    How can I make it smaller and keep everything else (i.e. checkmark) proportionate?

Follow this discussion

Leave a Comment

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>