Home

Awesome

Vue Global Loader

npm dependencies GitHub Workflow Status

Global loaders made easy for Vue and Nuxt.

Live DemoVite ExampleNuxt Example

<br />

:bulb: Please note that this package only works with Vite and Nuxt setups. Usage without a build-step is not supported.

<br />

Intro

I find it useful to display global loaders in single-page apps. For example:

Since I was re-creating the same logic and markup over and over again, I decided to publish a package for it.

Features

This package simplifies the usage of a single, top-level global loader by:

Table of Contents

Installation

pnpm add vue-global-loader
yarn add vue-global-loader
npm i vue-global-loader

Vite

:bulb: See ↓ below for Nuxt

main.js

import { createApp } from 'vue'
import { globalLoader } from 'vue-global-loader'

import App from './App.vue'

const app = createApp(App)

app.use(globalLoader, {
  // Options
})

app.mount('#app')

App.vue

<script setup>
import GlobalLoader from 'vue-global-loader/GlobalLoader.vue'
import CircleSpinner from 'vue-global-loader/CircleSpinner.vue'
</script>

<template>
  <GlobalLoader>
    <CircleSpinner />
  </GlobalLoader>

  <!-- RouterView -->
</template>

Nuxt

nuxt.config.ts

export default defineNuxtConfig({
  modules: ['vue-global-loader/nuxt'],
  globalLoader: {
    // Options
  }
})

app.vue

<template>
  <GlobalLoader>
    <CircleSpinner />
  </GlobalLoader>

  <!-- NuxtLayout, NuxtPage... -->
</template>

Usage

pages/login.vue

:bulb: No need to state the imports if using Nuxt (everything is auto-imported)

<script setup>
import { useRouter } from 'vue-router'
import { useGlobalLoader } from 'vue-global-loader'

const { displayLoader, destroyLoader, isLoading } = useGlobalLoader({
  screenReaderMessage:
    'Signing-in, redirecting to the dashboard, please wait...'
})

const router = useRouter()

async function signIn() {
  try {
    displayLoader() // Display loader...
    await auth.signIn()
    router.push('/dashboard')
  } catch (err) {
    console.error(err)
    destroyLoader()
  }
}
</script>

<template>
  <button :disabled="isLoading" @click="signIn">Sign-in</button>
</template>

pages/dashboard.vue

:bulb: No need to state the imports if using Nuxt (everything is auto-imported)

<script setup>
import { ref, onMounted } from 'vue'
import { useGlobalLoader } from 'vue-global-loader'

const { destroyLoader } = useGlobalLoader()

const data = ref(null)

onMounted(async () => {
  try {
    data.value = await fetchDashboardData()
    destroyLoader() // ...destroy loader, UI is ready
  } catch (err) {
    console.error(err)
    // Handle error
  }
})
</script>

<template>
  <div v-if="data">
    <!-- ... -->
  </div>
</template>

Customization

Spinners

This package ships with 8 spinners that should cover most use cases:

Import
spinnerimport CircleSpinner from 'vue-global-loader/CircleSpinner.vue'
ring-spinnerimport RingSpinner from 'vue-global-loader/RingSpinner.vue'
ring-dot-spinnerimport RingDotSpinner from 'vue-global-loader/RingDotSpinner.vue'
circle-bars-spinnerimport RingBarsSpinner from 'vue-global-loader/RingBarsSpinner.vue'
pulse-spinnerimport PulseSpinner from 'vue-global-loader/PulseSpinner.vue'
dots-spinnerimport DotsSpinner from 'vue-global-loader/DotsSpinner.vue'
bars-spinnerimport BarsSpinner from 'vue-global-loader/BarsSpinner.vue'
wave-spinnerimport WaveSpinner from 'vue-global-loader/WaveSpinner.vue'

Import the one you prefer and pass it to the default slot:

:bulb: No need to state the imports if using Nuxt (everything is auto-imported)

<script setup>
import GlobalLoader from 'vue-global-loader/GlobalLoader.vue'
import PulseSpinner from 'vue-global-loader/PulseSpinner.vue'
</script>

<template>
  <GlobalLoader>
    <PulseSpinner />
  </GlobalLoader>

  <!-- RouterView, NuxtLayout, NuxtPage... -->
</template>

Each spinner already has its own CSS and inherits the foregroundColor option specified in your config or scoped options.

If you think the spinner size is too big, add a class or inline styles to it and they'll be forwarded to the root svg element:

<template>
  <GlobalLoader>
    <PulseSpinner style="width: 60px" />
  </GlobalLoader>
</template>

Custom Spinners

To use your own spinner, pass a custom SVG (or whatever) to the default slot:

<template>
  <GlobalLoader>
    <svg
      class="MySpinner"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <!-- Inner markup -->
    </svg>
  </GlobalLoader>

  <!-- RouterView, NuxtLayout, NuxtPage... -->
</template>

<style>
.MySpinner {
  fill: var(--v-gl-fg-color); /* Value of the 'foregroundColor' prop */
  width: 100px;
  height: 100px;

  @media (min-width: 768px) {
    width: 160px;
    height: 100px;
  }
}
</style>

Options

PropTypeDescriptionDefault
screenReaderMessagestringMessage to announce when displaying the loader.Loading
transitionDurationnumberEnter/leave fade transition duration in ms. Set 0 to disable it.250
foregroundColorstringColor of the spinner.#000
backgroundColorstringBackground color of the loading screen.#fff
backgroundOpacitynumberBackground opacity of the loading screen.1
backgroundBlurnumberBackground blur of the loading screen.0
zIndexnumberZ-index of the loading screen.2147483647

Default Options

To customize defaults, pass the options to the globalLoader plugin (if using Vite):

main.js

app.use(globalLoader, {
  background: '#000',
  foreground: '#fff',
  screenReaderMessage: 'Loading, please wait...'
})

Or to the globalLoader config key (if using Nuxt):

nuxt.config.ts

export default defineNuxtConfig({
  modules: ['vue-global-loader/nuxt']
  globalLoader: {
    background: '#000',
    foreground: '#fff',
    screenReaderMessage: 'Loading, please wait...'
  }
})

Scoped Options

You can define options for a specific loader via useGlobalLoader options, only the loader triggered here (when calling displayLoader) will have a different backgroundOpacity and screenReaderMessage):

const { displayLoader, destroyLoader } = useGlobalLoader({
  backgroundOpacity: 0.5,
  screenReaderMessage: 'Redirecting to payment page, please wait...'
})

Updating default options

For convenience, you can set new defaults from anywhere in your Vue app using updateOptions:

:bulb: No need to state the imports if using Nuxt (everything is auto-imported)

<script setup>
import { watch } from 'vue'
import { useGlobalLoader } from 'vue-global-loader'
import { useStore } from '@/stores/my-store'
import messages from '@/locales/messages.json'

const store = useStore()

const { updateOptions } = useGlobalLoader()

// Kept in sync with an hypotetical store
watch(
  () => store.locale,
  (newLocale) => {
    updateOptions({
      screenReaderMessage: messages[newLocale].loading
    })
  },
  { immediate: true }
)
</script>

Callbacks / Transitions Lifecycle

The GlobalLoader lifecycle is handled using Vue's Transition hooks. For convenience, displayLoader and destroyLoader include some syntactic sugar to make it easier to execute code after the fade transition is completed.

displayLoader

This function returns a promise that resolves after the enter transition is completed or cancelled.

const { displayLoader } = useGlobalLoader()
const router = useRouter()

await displayLoader() // Wait for the fade transition to complete...
await signOut() // ...mutate the underlying UI
router.push('/') // ...navigate to the homepage and call 'destroyLoader' there

destroyLoader

This function doesn't return a promise but instead, it accepts a callback that is executed after the loader is destroyed.

const { destroyLoader } = useGlobalLoader()

destroyLoader(() => {
  console.log('Loader destroyed')
})

API

interface GlobalLoaderOptions {
  screenReaderMessage: string
  transitionDuration: number
  foregroundColor: string
  backgroundColor: string
  backgroundOpacity: number
  backgroundBlur: number
  zIndex: number
}

declare function useGlobalLoader(
  scopedOptions?: Partial<GlobalLoaderOptions>
): {
  displayLoader: () => Promise<void>
  destroyLoader: (onDestroyed?: () => void) => void
  updateOptions: (newOptions: Partial<GlobalLoaderOptions>) => void
  options: Readonly<Reactive<GlobalLoaderOptions>>
  isLoading: Readonly<Ref<boolean>>
}

Further Notes

When to use it

Use it when you think it's better for the user to not interact with the rest of the app or to not see what's happening in the UI while an expensive async operation initiated by the user is taking place.

When to not use it

SPA Loading Templates

For convenience, ready-made HTML templates for each spinner shipped with this package are available in this folder.

They can be used to display a global loader that has the same appearance of the one used in your app to be displayed while the app JS is loading.

Vite

Copy/paste the file content markup as a direct child of the body in the index.html file and remove it in a top-level (App.vue) onMounted hook via document.getElementById('spa_loading_template').remove().

Nuxt

Download the html file you prefer, rename it to spa-loading-template.html and place it in @/app/spa-loading-template.html.

Thanks

@n3r4zzurr0 for the awesome spinners.

License

MIT