vue-loading-overlay and vue-spinner are both Vue.js libraries designed to provide visual feedback during asynchronous operations, but they serve different use cases and offer distinct implementation models. vue-loading-overlay renders a full-screen or container-scoped semi-transparent overlay with a centered spinner, effectively blocking user interaction while indicating loading activity. In contrast, vue-spinner is a collection of lightweight, standalone animated spinner components that can be embedded directly into UI elements without blocking interaction. While both aim to improve user experience during data fetching or processing, their architectural approaches — modal blocking vs. inline decoration — lead to different integration patterns and user interface behaviors.
Both vue-loading-overlay and vue-spinner help communicate loading states in Vue applications, but they solve fundamentally different problems. One creates blocking overlays, the other provides decorative spinners. Understanding this distinction is key to choosing the right tool.
vue-loading-overlay shows a semi-transparent layer that covers part or all of the screen, disabling clicks underneath. This is useful when you must prevent user interaction.
<!-- vue-loading-overlay: blocks interaction -->
<template>
<div>
<loading :active="isLoading" :is-full-page="true" />
<button @click="submitForm">Submit</button>
</div>
</template>
<script>
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/vue-loading.css';
export default {
components: { Loading },
data() {
return { isLoading: false };
},
methods: {
async submitForm() {
this.isLoading = true;
await api.submit();
this.isLoading = false;
}
}
};
</script>
vue-spinner renders only the animation itself — no overlay, no pointer events blocking. Users can keep interacting with the page.
<!-- vue-spinner: purely visual -->
<template>
<div>
<clip-loader v-if="isLoading" color="#36d7b7" size="20px" />
<button @click="fetchData">Refresh</button>
</div>
</template>
<script>
import { ClipLoader } from 'vue-spinner';
export default {
components: { ClipLoader },
data() {
return { isLoading: false };
},
methods: {
async fetchData() {
this.isLoading = true;
await api.getData();
this.isLoading = false;
}
}
};
</script>
💡 Rule of thumb: If you’re disabling a button and showing a spinner, you probably don’t need an overlay. If you’re preventing navigation or form edits during a save, use an overlay.
vue-loading-overlay is typically used as a managed component whose visibility is controlled by a boolean prop (active). It handles DOM positioning, z-index stacking, and accessibility attributes automatically.
// Can also be used programmatically via plugin
import Loading from 'vue-loading-overlay';
Vue.use(Loading);
// Then in component:
this.$loading.show();
setTimeout(() => this.$loading.hide(), 2000);
vue-spinner exports individual spinner components (e.g., ScaleLoader, PulseLoader) that you drop into templates like any other presentational element. There’s no global state or plugin system — just props for color, size, and speed.
<template>
<div class="card">
<h3>User Profile</h3>
<scale-loader v-if="profileLoading" color="#42b983" />
<user-details v-else :user="user" />
</div>
</template>
<script>
import { ScaleLoader } from 'vue-spinner';
export default { components: { ScaleLoader } };
</script>
vue-loading-overlay allows customization of the overlay background, spinner type, and text via props:
<loading
:active="isLoading"
background-color="rgba(0,0,0,0.8)"
loader="dots"
:text="'Processing...'"
/>
It supports built-in loaders like spinner, dots, and bars, but you cannot easily inject custom SVG animations.
vue-spinner gives you fine control over animation appearance through CSS-compatible props:
<rotate-loader
:loading="isLoading"
:color="'#ff6b6b'"
:size="'15px'"
:margin="'2px'"
/>
Each spinner is a self-contained component with its own animation logic, making it easy to match brand colors or adjust sizing without affecting global styles.
vue-loading-overlay automatically sets aria-busy="true" on the overlay and manages focus trapping in full-page mode, which helps screen reader users understand the page is temporarily unavailable.
vue-spinner components do not include ARIA attributes by default. You must add them manually if needed:
<clip-loader
v-if="loading"
aria-label="Loading user data"
role="status"
/>
This reflects its philosophy: it’s a visual primitive, not a complete UX solution.
vue-loading-overlay when:vue-spinner when:As of 2024, both packages are actively maintained with recent releases supporting Vue 3 (via compatibility builds or dedicated versions). Neither is deprecated, and both provide clear migration paths for Vue 2 → Vue 3 projects.
| Feature | vue-loading-overlay | vue-spinner |
|---|---|---|
| Blocks interaction | ✅ Yes (via overlay) | ❌ No |
| Use case | Modal-like loading | Inline decorative spinners |
| API style | State-driven component or plugin | Stateless presentational components |
| Custom animations | ❌ Limited to built-in types | ✅ Via props (color, size, speed) |
| Accessibility | ✅ Built-in ARIA support | ❌ Manual implementation required |
| Bundle impact | Larger (includes overlay logic) | Smaller (only spinner SVG/CSS) |
Don’t think of these as competitors — they’re complementary tools. Many mature Vue apps use both: vue-spinner for inline micro-interactions and vue-loading-overlay for critical, blocking operations. Choose based on whether you need to prevent user action (overlay) or simply communicate activity (spinner).
Choose vue-loading-overlay when you need to prevent user interaction during critical background operations (e.g., form submission, data sync) and want a consistent, full-screen or container-level loading state. It’s ideal for scenarios where you must ensure users don’t trigger additional actions while waiting, and you prefer a declarative API that ties loading visibility directly to component state or Vuex stores. Its overlay-based approach works well in admin panels, checkout flows, or any context requiring modal-like loading behavior.
Choose vue-spinner when you need lightweight, non-blocking spinners that integrate seamlessly into buttons, cards, or inline elements without disrupting the layout or user flow. It’s best suited for progressive disclosure patterns — like lazy-loaded sections or async search results — where multiple independent loading states coexist. Since it provides pure presentational components with no DOM manipulation or z-index management, it pairs well with custom UI systems that already handle loading logic externally.
Vue.js component for full screen loading indicator
| Vue.js version | Package version | Branch |
|---|---|---|
| 2.x | 3.x | 3.x |
| 3.x | 6.x | main |
npm install vue-loading-overlay@^6.0
<template>
<div class="vl-parent">
<loading v-model:active="isLoading"
:can-cancel="true"
:on-cancel="onCancel"
:is-full-page="fullPage"/>
<label><input type="checkbox" v-model="fullPage">Full page?</label>
<button @click.prevent="doAjax">fetch Data</button>
</div>
</template>
<script>
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css';
export default {
data() {
return {
isLoading: false,
fullPage: true
}
},
components: {
Loading
},
methods: {
doAjax() {
this.isLoading = true;
// simulate AJAX
setTimeout(() => {
this.isLoading = false
}, 5000)
},
onCancel() {
console.log('User cancelled the loader.')
}
}
}
</script>
Initialise the plugin in your app
import {createApp} from 'vue';
import {LoadingPlugin} from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css';
// Your app initialization logic goes here
const app = createApp({});
app.use(LoadingPlugin);
app.mount('#app');
Then use the plugin in your components
<template>
<form @submit.prevent="submit"
class="vl-parent"
ref="formContainer">
<!-- your form inputs goes here-->
<label><input type="checkbox" v-model="fullPage">Full page?</label>
<button type="submit">Login</button>
</form>
</template>
<script>
export default {
data() {
return {
fullPage: false
}
},
methods: {
submit() {
let loader = this.$loading.show({
// Optional parameters
container: this.fullPage ? null : this.$refs.formContainer,
canCancel: true,
onCancel: this.onCancel,
});
// simulate AJAX
setTimeout(() => {
loader.hide()
}, 5000)
},
onCancel() {
console.log('User cancelled the loader.')
}
}
}
</script>
<script setup>
import {ref, inject} from 'vue'
import {useLoading} from 'vue-loading-overlay'
const $loading = useLoading({
// options
});
// or use inject without importing useLoading
// const $loading = inject('$loading')
const fullPage = ref(false)
const submit = () => {
const loader = $loading.show({
// Optional parameters
});
// simulate AJAX
setTimeout(() => {
loader.hide()
}, 5000)
}
</script>
The component accepts these props:
| Attribute | Type | Default | Description |
|---|---|---|---|
| active | Boolean | false | Show loading by default when true, use it as v-model:active |
| can-cancel | Boolean | false | Allow user to cancel by pressing ESC or clicking outside |
| on-cancel | Function | ()=>{} | Do something upon cancel, works in conjunction with can-cancel |
| is-full-page | Boolean | true | When false; limit loader to its container^ |
| transition | String | fade | Transition name |
| color | String | #000 | Customize the color of loading icon |
| height | Number | * | Customize the height of loading icon |
| width | Number | * | Customize the width of loading icon |
| loader | String | spinner | Name of icon shape you want use as loader, spinner or dots or bars |
| background-color | String | #fff | Customize the overlay background color |
| opacity | Number | 0.5 | Customize the overlay background opacity |
| z-index | Number | 9999 | Customize the overlay z-index |
| enforce-focus | Boolean | true | Force focus on loader |
| lock-scroll | Boolean | false | Freeze the scrolling during full screen loader |
is-full-page is set to false, the container element should be positioned as position: relative. You can
use CSS helper class vl-parent.height and width values may vary based on the loader prop valueThe component accepts these slots:
default - Replace the animated icon with yoursbefore - Place anything before the animated icon, you may need to style this.after - Place anything after the animated icon, you may need to style this.this.$loading.show(?propsData,?slots)import {h} from 'vue';
let loader = this.$loading.show({
// Pass props by their camelCased names
container: this.$refs.loadingContainer,
canCancel: true, // default false
onCancel: this.yourCallbackMethod,
color: '#000000',
loader: 'spinner',
width: 64,
height: 64,
backgroundColor: '#ffffff',
opacity: 0.5,
zIndex: 999,
}, {
// Pass slots by their names
default: h('your-custom-loader-component-name'),
});
// hide loader whenever you want
loader.hide();
You can set props and slots for all future instances when using as plugin
app.use(LoadingPlugin, {
// props
color: 'red'
}, {
// slots
})
Further you can override any prop or slot when creating new instances
let loader = this.$loading.show({
color: 'blue'
}, {
// override slots
});
<!-- Vue js -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3"></script>
<!-- Lastly add this package -->
<script src="https://cdn.jsdelivr.net/npm/vue-loading-overlay@6"></script>
<link href="https://cdn.jsdelivr.net/npm/vue-loading-overlay@6/dist/css/index.css" rel="stylesheet">
<!-- Init the plugin and component-->
<script>
const app = Vue.createApp({});
app.use(VueLoading.LoadingPlugin);
app.component('loading', VueLoading.Component)
app.mount('#app')
</script>
>=20.11 and pnpm >=8.x pre-installedpnpm installnpm startnpm testMIT License