How to add offline support in Vue

Offline support enables Vue applications to work without internet connectivity by caching assets and storing data locally. As the creator of CoreUI with 12 years of Vue development experience, I’ve built Vue offline-first applications that provide seamless experiences even in areas with poor connectivity.

The most reliable approach combines service workers for asset caching with IndexedDB or localStorage for data persistence.

Setup Service Workers

First, add PWA support (see how to add PWA support in Vue).

Configure offline caching in vite.config.js:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    vue(),
    VitePWA({
      workbox: {
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/api\.example\.com\/.*/i,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api-cache',
              networkTimeoutSeconds: 10,
              expiration: {
                maxEntries: 50,
                maxAgeSeconds: 60 * 60 * 24
              }
            }
          },
          {
            urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
            handler: 'CacheFirst',
            options: {
              cacheName: 'image-cache',
              expiration: {
                maxEntries: 100,
                maxAgeSeconds: 60 * 60 * 24 * 30
              }
            }
          }
        ]
      }
    })
  ]
})

Create Offline Composable

Create src/composables/useOffline.js:

import { ref, onMounted, onUnmounted } from 'vue'

export const useOffline = () => {
  const isOnline = ref(navigator.onLine)

  const updateOnlineStatus = () => {
    isOnline.value = navigator.onLine
  }

  onMounted(() => {
    window.addEventListener('online', updateOnlineStatus)
    window.addEventListener('offline', updateOnlineStatus)
  })

  onUnmounted(() => {
    window.removeEventListener('online', updateOnlineStatus)
    window.removeEventListener('offline', updateOnlineStatus)
  })

  return { isOnline }
}

Create Storage Service

Create src/services/offlineStorage.js:

class OfflineStorage {
  constructor(storeName = 'offline_data') {
    this.storeName = storeName
  }

  save(key, data) {
    const store = this.getStore()
    store[key] = {
      data,
      timestamp: Date.now()
    }
    localStorage.setItem(this.storeName, JSON.stringify(store))
  }

  get(key) {
    const store = this.getStore()
    return store[key]?.data || null
  }

  getWithTimestamp(key) {
    const store = this.getStore()
    return store[key] || null
  }

  remove(key) {
    const store = this.getStore()
    delete store[key]
    localStorage.setItem(this.storeName, JSON.stringify(store))
  }

  clear() {
    localStorage.removeItem(this.storeName)
  }

  getStore() {
    const stored = localStorage.getItem(this.storeName)
    return stored ? JSON.parse(stored) : {}
  }

  isStale(key, maxAge = 3600000) {
    const item = this.getWithTimestamp(key)
    if (!item) return true
    return Date.now() - item.timestamp > maxAge
  }
}

export default new OfflineStorage()

Create Offline-Aware API Service

Create src/services/api.js:

import offlineStorage from './offlineStorage'

class ApiService {
  async fetch(url, options = {}) {
    const cacheKey = `api:${url}`

    if (!navigator.onLine) {
      const cached = offlineStorage.get(cacheKey)
      if (cached) {
        return cached
      }
      throw new Error('No internet connection and no cached data')
    }

    try {
      const response = await fetch(url, options)
      const data = await response.json()

      offlineStorage.save(cacheKey, data)
      return data
    } catch (error) {
      const cached = offlineStorage.get(cacheKey)
      if (cached) {
        console.warn('Using cached data due to network error')
        return cached
      }
      throw error
    }
  }

  async get(url) {
    return this.fetch(url)
  }

  async post(url, body) {
    if (!navigator.onLine) {
      this.queueRequest('POST', url, body)
      throw new Error('Request queued for when online')
    }

    return this.fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body)
    })
  }

  queueRequest(method, url, data) {
    const queue = JSON.parse(localStorage.getItem('request_queue') || '[]')
    queue.push({ method, url, data, timestamp: Date.now() })
    localStorage.setItem('request_queue', JSON.stringify(queue))
  }

  async processQueue() {
    const queue = JSON.parse(localStorage.getItem('request_queue') || '[]')

    for (const request of queue) {
      try {
        await this.fetch(request.url, {
          method: request.method,
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(request.data)
        })
      } catch (error) {
        console.error('Failed to process queued request:', error)
        break
      }
    }

    localStorage.removeItem('request_queue')
  }
}

export default new ApiService()

Use in Component

<script setup>
import { ref, onMounted, watch } from 'vue'
import { useOffline } from '@/composables/useOffline'
import api from '@/services/api'

const { isOnline } = useOffline()
const users = ref([])
const loading = ref(false)
const error = ref(null)

const fetchUsers = async () => {
  loading.value = true
  error.value = null

  try {
    users.value = await api.get('/api/users')
  } catch (err) {
    error.value = err.message
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  fetchUsers()
})

watch(isOnline, (online) => {
  if (online) {
    api.processQueue()
    fetchUsers()
  }
})
</script>

<template>
  <div>
    <div v-if="!isOnline" class="offline-banner">
      You are offline. Showing cached data.
    </div>

    <div v-if="loading">Loading...</div>
    <div v-if="error" class="error">{{ error }}</div>

    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>

<style scoped>
.offline-banner {
  background: #ff9800;
  color: white;
  padding: 12px;
  text-align: center;
  margin-bottom: 16px;
}

.error {
  background: #f44336;
  color: white;
  padding: 12px;
  border-radius: 4px;
}
</style>

Background Sync

For form submissions when offline:

// In service worker
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-forms') {
    event.waitUntil(syncForms())
  }
})

async function syncForms() {
  const queue = await getQueuedForms()

  for (const form of queue) {
    try {
      await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(form.data)
      })
      await deleteFromQueue(form.id)
    } catch (error) {
      console.error('Sync failed:', error)
      break
    }
  }
}

Best Practice Note

This is the same offline strategy we use in CoreUI’s Vue admin templates. Combining service workers for asset caching with localStorage or IndexedDB for data ensures your app works seamlessly offline. Always implement request queuing for write operations performed while offline.

For production applications, consider using CoreUI’s Vue Admin Template which includes pre-built offline support with background sync and conflict resolution.

For complete PWA functionality, you might also want to learn how to use Vue with Service Workers and how to use Vue with IndexedDB.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author