VueJS loading button with TailwindCSS
Loading buttons. We need them in every project. We submit the form, it takes a while to finish, so we display a spinner or something as feedback that says, "Hey, I am working, just give me a second to complete."
So in this post, we'll take a look over how we can make a super simple, clean, loading button that, when active, shows a spinner in the middle of it.
The first thing I'm going to do is style the button to make it pretty.
1<template>2 <button class="px-4 py-2 text-sm font-medium rounded text-white bg-rose-600 hover:bg-rose-600/80">3 Update profile4 </button>5</template>
Next up, we need to add the SVG icon representing the loader. Then, we'll set a width and a height, make it white, and then we'll make it spin using the animate-spin
class.
1<template> 2 <button class="px-4 py-2 text-sm font-medium rounded text-white bg-rose-600 hover:bg-rose-600/80"> 3 4 <svg class="w-5 h-5 text-white animate-spin" fill="none" 5 viewBox="0 0 24 24" 6 xmlns="http://www.w3.org/2000/svg"> 7 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> 8 <path class="opacity-75" 9 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"10 fill="currentColor"></path>11 </svg>12 13 Update profile14 </button>15</template>
What I want to do is to position the spinner in the middle of the button. One solution is to add relative
to our button and then make the spinner absolute
, push it to the right 50% of the way using left-1/2
, and then drag it back half of its size. Since our spinner is w-5
, we need to add a negative left margin of -ml-5
. This will place the spinner dead center.
Now, all we need to do is hide the text and show the spinner when the button is loading. To do that, I'll add an isLoading reactive value, set its default to true, and then use v-show to toggle the spinner and the button label (which we'll wrap within a span
tag).
1<script setup> 2const isLoading = ref(true); 3</script> 4 5<template> 6 <button class="px-4 py-2 text-sm font-medium rounded text-white bg-rose-600 hover:bg-rose-600/80"> 7 <svg v-show="isLoading" class="w-5 h-5 text-white animate-spin" fill="none" 8 viewBox="0 0 24 24" 9 xmlns="http://www.w3.org/2000/svg">10 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>11 <path class="opacity-75"12 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"13 fill="currentColor"></path>14 </svg>15 <span v-show="isLoading">Update profile</span>16 </button>17</template>
However, when v-show
is true, it basically translates to display:none;
, which breaks our button because there's nothing inside it to maintain its size. To fix that, instead of using v-show
to toggle the button label, we'll toggle the invisible
class.
1<script setup> 2const isLoading = ref(true); 3</script> 4 5<template> 6 <button class="px-4 py-2 text-sm font-medium rounded text-white bg-rose-600 hover:bg-rose-600/80"> 7 <svg v-show="isLoading" class="w-5 h-5 text-white animate-spin" fill="none" 8 viewBox="0 0 24 24" 9 xmlns="http://www.w3.org/2000/svg">10 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>11 <path class="opacity-75"12 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"13 fill="currentColor"></path>14 </svg>15 <span :class="{'invisible': isLoading}">Update profile</span>16 </button>17</template>
Next up, we should turn isLoading
into a prop passed by whoever uses this loading button component. We can do that using the defineProps
vue compiler macro.
1<script setup> 2defineProps({ 3 isLoading: { 4 type: Boolean, 5 default: false 6 } 7}); 8</script> 9 10<template>
11 <button class="px-4 py-2 text-sm font-medium rounded text-white bg-rose-600 hover:bg-rose-600/80">12 <svg v-show="isLoading" class="w-5 h-5 text-white animate-spin" fill="none"13 viewBox="0 0 24 24"14 xmlns="http://www.w3.org/2000/svg">15 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>16 <path class="opacity-75"17 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"18 fill="currentColor"></path>19 </svg>20 <span :class="{'invisible': isLoading}">Update profile</span>21 </button>22</template>
Another thing we should do is allow whoever uses the loading button component to set the button label. And we can do that by replacing the current "Update profile" text with a slot
tag.
1<script setup> 2defineProps({
3 isLoading: { 4 type: Boolean, 5 default: false 6 } 7}); 8</script> 9 10<template>11 <button class="px-4 py-2 text-sm font-medium rounded text-white bg-rose-600 hover:bg-rose-600/80">12 <svg v-show="isLoading" class="w-5 h-5 text-white animate-spin" fill="none"viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
13 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>14 <path class="opacity-75"15 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"16 fill="currentColor"></path>17 </svg> 18 <span :class="{'invisible': isLoading}">19 <slot></slot>20 </span>21 </button>22</template>
Finally, currently, there is no way to change the button's color. We should look inside the component, decide which styles might vary, and extract them outside the component. In our case, we want to remove the following classes: text-white bg-rose-600 hover:bg-rose-600/80
.
1<script setup> 2defineProps({ 3 isLoading: { 4 type: Boolean, 5 default: false 6 } 7}); 8</script> 9 10<template>11 <button class="px-4 py-2 text-sm font-medium rounded">12 <svg v-show="isLoading" class="w-5 h-5 text-white animate-spin" fill="none"13 viewBox="0 0 24 24"14 xmlns="http://www.w3.org/2000/svg">15 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>16 <path class="opacity-75"17 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"18 fill="currentColor"></path>19 </svg>20 <span :class="{'invisible': isLoading}">21 <slot></slot>22 </span>23 </button>24</template>
This will allow us to set these styles from the outside:
1// Some other component2 3<LoadingButton :isLoading="isSubmitting" class="text-white bg-black hover:bg-black/80">4 Save changes5</LoadingButton>
And that's it; I hope you found this post useful! Check the full snippets in the "Code" section above.
-
1Managing SVG icons04:57
-
2Programmatically focus elements to provide a better user experience08:29
-
3Building a CopyToClipboard renderless component in VueJS08:29
-
4Create reusable form components using Vuejs 3 multiple v-models06:58
-
5Vue 3: ref and reactive05:34
-
6Loading scripts inside Vue components08:14
-
7Google maps autocomplete with VueJS10:52
-
8From options API to script setup using the composition API07:04
-
VueJS loading button with TailwindCSS06:48
-
10Component communication in Vue3: sibling, parent, and child components06:50
-
11Building a Vue JS textarea autoresize component05:13
-
12Vue watch multiple values at once03:55
-
13Building a Vue tooltip component using TailwindCSS and Floating UI16:27
-
14Vue templates: avoid functions, use computed properties instead05:12
-
15Build an Alert component using Vue, TailwindCSS and CVA17:23
-
16When to pass function props in vue 302:35
-
17Reusable avatar component11:23
-
18Table of Contents component using VueJS13:43
-
19Cleaner form fields in VueJS14:54
-
20Button component12:16
-
21Vue async combobox component19:14
-
22Vue State and LocalStorage: Perfect Sync Made Simple!07:44
-
23Nuxt 3 environment variables config08:36
-
24Streamline Vue Forms with this useForm composable07:36
-
1Managing SVG icons04:57
-
2Programmatically focus elements to provide a better user experience08:29
-
3Building a CopyToClipboard renderless component in VueJS08:29
-
4Create reusable form components using Vuejs 3 multiple v-models06:58
-
5Vue 3: ref and reactive05:34
-
6Loading scripts inside Vue components08:14
-
7Google maps autocomplete with VueJS10:52
-
8From options API to script setup using the composition API07:04
-
VueJS loading button with TailwindCSS06:48
-
10Component communication in Vue3: sibling, parent, and child components06:50
-
11Building a Vue JS textarea autoresize component05:13
-
12Vue watch multiple values at once03:55
-
13Building a Vue tooltip component using TailwindCSS and Floating UI16:27
-
14Vue templates: avoid functions, use computed properties instead05:12
-
15Build an Alert component using Vue, TailwindCSS and CVA17:23
-
16When to pass function props in vue 302:35
-
17Reusable avatar component11:23
-
18Table of Contents component using VueJS13:43
-
19Cleaner form fields in VueJS14:54
-
20Button component12:16
-
21Vue async combobox component19:14
-
22Vue State and LocalStorage: Perfect Sync Made Simple!07:44
-
23Nuxt 3 environment variables config08:36
-
24Streamline Vue Forms with this useForm composable07:36