#app
	zz-button-progress(
		text="Upload",
		ref="zzUpload",
		v-on:progress="moveProgress"
		v-on:progress-finished="endProgress"
	)
View Compiled
$base-color: white;
$bg-full: #2f8aff;
$bg-empty: #085dc9;
$bg-done: #52b500;
$default-font-size-label: 32px;
$default-width: 250px;
$default-height: 100px;
$active-font-size-label: 18px;
$active-font-size-progress: 48px;
$active-width-height: 175px;
$done-check-thickness: 10px;
$done-check-width: 50px;
$done-check-height: 20px;
$animation-time: 750ms;
$animation-style: cubic-bezier(0.7, -0.55, 0.3, 1.55);

@import url("https://fonts.googleapis.com/css?family=Raleway:400,400i,700");

html, body {
	width: 100%;
	height: 100%;
	padding: 0;
	margin: 0;
}

body {
	font-family: Raleway, sans-serif;
	display: flex;
	align-items: center;
	justify-content: center;
	background-color: #424242;
}

.zz-button {
	color: $base-color;
	font-family: inherit;
	font-size: $default-font-size-label;
	font-weight: bold;
	background-color: $bg-full;
	width: $default-width;
	height: $default-height;
	border: none;
	cursor: pointer;
	outline: none;
	border-radius: 20% / 50%;
	transition: color $animation-time ease-in-out,
		font-size $animation-time $animation-style,
		background-color $animation-time ease-in-out,
		width $animation-time $animation-style,
		height $animation-time $animation-style,
		border-radius $animation-time $animation-style;
}

.zz-button:after {
	content: '0%';
	font-size: 0;
	display: block;
	transition: font-size $animation-time ease-in-out;
}

.zz-button.active {
	font-size: $active-font-size-label;
	background-color: $bg-empty;
	width: $active-width-height;
	height: $active-width-height;
	border-radius: 50%;
}

.zz-button.active:after {
	font-size: $active-font-size-progress;
}

@for $i from 0 through 100 {
	.zz-button[zz-button-progress='#{$i}'] {
		background-image: linear-gradient(to top, $bg-full $i * 1%, $bg-empty $i * 1%);
	}
	
	.zz-button[zz-button-progress='#{$i}']:after {
		content: '#{$i}%';
	}
}

.zz-button[zz-button-progress='100'] {
	animation: progress-done-pre $animation-time ease-out;
}

@keyframes progress-done-pre {
	0%   { transform: scale(1); }
	35%  { transform: scale(1.15); }
	75%  { transform: scale(0.9); }
	90%  { transform: scale(1.05) }
	100% { transform: scale(1); }
}

.zz-button-progress-done {
	color: transparent;
	background-color: $bg-done;
	position: relative;
}

.zz-button-progress-done:before {
	content: '';
	width: 0px;
	height: 0px;
	display: block;
}

.zz-button-progress-done.zz-button-progress-done-active:before {
	width: $done-check-width;
	height: $done-check-height;
	border: solid $done-check-thickness $base-color;
	border-top: none;
	border-right: none;
	position: absolute;
	top: ($default-height / 2);
	left: ($default-width / 2) - ($done-check-width / 2);
	transform: rotate(-45deg);
	transform-origin: top left;
	animation: progress-done-post $animation-time $animation-style;
}

@keyframes progress-done-post {
	0%   { width: 0px; height: 0px; }
	50%  { width: 0px; height: $done-check-height; }
	100% { width: $done-check-width; height: $done-check-height; }
}
View Compiled
// ----- The component

Vue.component('zz-button-progress', {
	props: ['text'],
	data: function() {
		return {
			progress: null,
			active: false,
			done: false,
			check: false
		}
	},
	methods: {
		/**
		 * Changes the value of the attribute 'zz-button-progress'
		 * Changing its value will change the percentage of the
		 * progress of the button.
		 */
		moveProgress: function(progress) {
			this.progress = progress;
		},
		/**
		 * Reset the button progress to its original state.
		 */
		resetProgress: function() {
			this.progress = null;
			this.active   = false;
			this.done     = false;
			this.check    = false;
		},
		/**
		 * Event handler for click event
		 */
		progressClickEvent: function() {
			if (this.active === false && this.done === false) {
				this.active = true;
			}
		},
		/**
		 * Event handler for transitionend event
		 */
		progressTransitionEndEvent: function(evt) {
			if (this.progress === null && this.active === true) {
				this.progress = 0;
				
				this.$emit('progress');
			}
			else if (this.done === true) {
				this.check = true;
			}
		},
		/**
		 * Event handler for animationend event
		 */
		progressAnimationEndEvent: function(evt) {
			if (evt.animationName == 'progress-done-pre') {
				this.done     = true;
				this.active   = false;
				this.progress = null;
			}
			else if (evt.animationName == 'progress-done-post') {
				this.$emit('progress-finished');
			}
		}
	},
	template: `
<button
	v-bind:zz-button-progress="this.progress"
	v-bind:class="['zz-button', {'active': active, 'zz-button-progress-done': done, 'zz-button-progress-done-active': check}]"
	v-on:click="progressClickEvent"
	v-on:transitionend="progressTransitionEndEvent"
	v-on:animationend="progressAnimationEndEvent"
>
	{{ this.text }}
</button>
`
});

// ----- VueJS VM

new Vue({
	el: '#app',
	methods: {
		moveProgress: function(progress = 0) {
			// This is just an example progress.
			// In real-world application, return from ajax process may be used
			var progressTimeout = setTimeout(() => {
				clearTimeout(progressTimeout);
				
				if (progress < 100) {
					let newProgress = progress + 1;
					this.$refs.zzUpload.moveProgress(newProgress);
					this.moveProgress(newProgress);
				}
			}, 10);
		},
		endProgress: function() {
			var endProgressTimeout = setTimeout(() => {
				clearTimeout(endProgressTimeout);
				
				this.$refs.zzUpload.resetProgress();
			}, 5000);
		}
	}
});
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js