<div id="app">
	<button class="o-btn" 
			:class="{
				'o-btn--pending': status === 'pending'
			}"
			:disabled="status !== 'default'"
			@click.prevent="change"
	>
		Click to change state
	</button>
	<div class="o-status" 
		 role="status"
		 v-if="status !== 'default'"
		 :class="{
			 'o-status--pending': status === 'pending',
			 'o-status--success': status === 'success',
			 'o-status--error': status === 'error'
		 }">
		<span class="o-status__inner">
			<span class="o-status__text">{{ statusText }}</span>
			<span class="[ o-status__icon ] [ fa ]"
				  :class="{
					  'fa-arrow-right': status === 'default',
					  'fa-hourglass': status === 'pending',
					  'fa-check': status === 'success',
					  'fa-exclamation-circle': status === 'error'
				  }"
				  aria-hidden="true" 
				  role="presentation">
			</span>
		</span>
	</div>
</div>
// Vars 
$primary: #34495e;
$primary--light: #527395;
$secondary: #d35400;
$radius: 3px;
$transition: all 250ms cubic-bezier(0.675, 0.420, 0.090, 0.870);

// Core
body {
	font-size: 16px;
	background: #f3f3f3;
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
	position: relative;
	font-family: 'Lato';
}

// Button object 
.o-btn {
	display: inline-block;
	appearance: none;
	border: none;
	padding: 0;
	margin: 0;
	text-decoration: none;
	font-family: 'Lato';
	font-weight: 900;
	font-size: 1.3rem;
	line-height: 1;
	padding: 15px 25px 17px 25px;
	border-radius: $radius;
	background: $primary;
	color: #fff;
	cursor: pointer;
	transition: $transition;

	&:not([disabled]):hover,
	&:not([disabled]):focus {
		background: $primary--light;
	}
	
	&:focus {
		outline: none;
		box-shadow: 0 0 0 3px $secondary;
	}
	
	&[disabled] {
		cursor: not-allowed;
	}
	
	// State colours 
	&--pending {
		background: #7f8c8d;
	}
}


// Status object
.o-status {
	padding: 5px 10px;
	margin: 10px 0 0 0;
	color: white;
	font-family: 'Lato';
	transition: $transition;
	border-radius: $radius;
	text-align: center;
	position: absolute; // Keep current position, but this stops the button from moving
	
	&__inner {
		display: flex;
		align-items: center;
		justify-content: center;
	}
	
	&__icon {
		padding-left: 10px;
	}
	
	// State colours 
	&--pending {
		background: #7f8c8d;
	}
	
	&--success {
		background: #27ae60;
	}
	
	&--error {
		background: #c0392b;
	}
}
View Compiled
var App = new Vue({
	el: '#app',
	
	data: {
		status: 'default'
	},
	
	computed: {
		
		// Return state text based on current state
		statusText() {
			switch(this.status) {
				case 'pending':
					return 'Sending. Please wait';
				case 'error':
					return 'There was an error';
				case 'success':
					return 'Yay! Sent successfully';
			}
		}
	},
	
	methods: {
		change() {
			let self = this;
			
			self.status = 'pending';
			
			// Set a timeout to simulate loading
			setTimeout(() => {
				
				// Pick a response status at random
				self.status = Math.random() < 0.5 ? 'error' : 'success';
				
				// Reset after a timeout
				setTimeout(() => {
					self.status = 'default';
				}, 1500);
			}, 3000);
		}	
	}
})
View Compiled

External CSS

  1. https://codepen.hankchizljaw.io/css/global.css
  2. https://fonts.googleapis.com/css?family=Fira+Mono|Lato:400,900
  3. https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css

External JavaScript

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