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.

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!

Tagged with:

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

Stay up to date with the latest web design and development news and relevant updates from Codrops.

Feedback 47

Comments are closed.
  1. Mary Lou, thank you. You are my only guide in world of modern web design. Thank you for sharing all that amazing stuff with us.

  2. I was blown away by the progress button styles that were shared not so long ago, and this is another one that I’m going to add to my collection of epic material. Mary, you’ve inspired me to look at some of my left alone projects and give them a second chance, thanks a lot!

    – Shawn

  3. Great example.

    Not it would be nice to implement to GravityForms in WordPress? Any idea how to do it?

  4. Love the idea, Mary. I’ll try to adapt it for a picture upload. ๐Ÿ™‚

    Cheeeers

  5. From where do you get these kind of great inspiration & ideas to work on! Fabulous. Good Job.

  6. Hi Mary Lou

    I’ve been following you for many years now and want to commend you on being such a wonderfully gifted creative. Keep up the good work

  7. Excellent work…. What code I have to type to use it in asp.net for saving data….???

    • You must open a CMD window, type format C and press enter… now seriously, learn to code yourself instead of asking someone to do all the job for you.

  8. Great example Mary Lou.
    you’re really a unique guide towards modern web design. thanks a lot!

  9. 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!!!

  10. 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

  11. 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

    • Add this to your AJAX call

      beforeSend: function(){
      // do stuff while sending data
      },
      success: function(){
      // do stuff when response is received from server
      }

  12. 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?

  13. First of all thanks for the tutorial, it was very helpful. I have two questions if you don’t mind.

    The first is whether this control can be used with an asp form. I am not very pro efficient in the subject and the only workaround I could come up with is to perform javascript validation instead and imitate a click afterwards. Is there some other neater way? Also, I would like to ask where should the validation be included. I downloaded the source and couldn’t identify where the code should be placed.

    Thanks! ๐Ÿ™‚

  14. How To Add A Function When The Progress Completes Or Ends , I just Want To add A Function To it when it completes

  15. Awesome tutorial. I saw a corporate site in my country using the circular progress button exactly as explained in this tutorial hehe.
    Thanks for the tutorial and excellent site by the way.

  16. [http://editor.method.ac/] the website report error , so we can’t make use of content of the website , therefore i hope the owner of the website can fix the problem quickly , thx

  17. How can i incorporate this into my current contact form. I have incorporated AJAX in to page and still have animation but its not actually going to php form

  18. Hi,
    I wonder if those type of animation could also be done for mobile developers. As far as I know this would be quite hard.

  19. Bug with Matรฉrialize framework path install with bower : bower_components/Materialize/dist/css/materialize.css

    Problem

    i don’t see the circle animation and check animation

    Solution -> disable position relative

    .waves-effect {
    /* position: relative; */
    cursor: pointer;
    display: inline-block;
    overflow: hidden;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    vertical-align: middle;
    z-index: 1;
    will-change: opacity, transform;
    transition: all .3s ease-out;
    }

    ๐Ÿ˜‰

  20. How to load this morphing button to the page loading through ajax call and content from js file

  21. Thanks for the great article.

    How do I set the Status (ie Success or Error) when the progress circle completes?

    I have a form that I want to submit, via Ajax. I’d like to show the Error (“cross”), is the call fails, and the Success (“checkmark”) when the call/post is successful.

    It’s not obvious to me how to achieve this with the code above.

    Thanks in advance.