npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vue-smart-loading-kit

v0.3.4

Published

Turn your real Vue components into their own skeleton with one directive, plus spinners, progress bars and a loader that never flashes

Readme

Vue Smart Loading Kit

Turn your real Vue components into their own skeleton with one directive, plus spinners, progress bars and a loader that never flashes.

<UserCard v-skeleton="loading" :user="user" />

npm version npm downloads CI coverage mutation score license

▶ Live demo: every component with live controls and copyable code.

Why this kit

  • v-skeleton turns your real component into its skeleton. There is no second, hand-built placeholder to keep in sync. It's CSS only, with no build step, no measuring and no DOM changes, so nothing on the page moves and it works with SSR.
  • Loaders that never flash. SmartLoader waits 200ms before showing anything, then keeps the loader for at least 500ms. Error state and retry are built in.
  • A replacement for NProgress. PageProgress follows vue-router and your own requests, and counts loads that run in parallel.

✨ Features

  • Built for Vue 3
  • Lightweight and easy to integrate
  • Multiple Skeleton variants
  • Multiple Spinner variants, including a determinate progress arc
  • ProgressBar: a linear progress indicator, determinate or indeterminate
  • SmartLoader: no flash for fast loads, no blink for slow ones
  • PageProgress: a top-of-page bar for route changes and requests, replacing NProgress
  • v-skeleton: turns your real content into a matching skeleton, with no hand-built placeholders
  • Global config: set app-wide defaults once
  • Customizable size, radius, colors, animation, and more
  • Responsive by default
  • TypeScript support
  • Works with both local and global component registration
  • No runtime dependency on Vue — Vue is provided as a peer dependency
  • MIT licensed

📦 Installation

Install the package using npm:

npm install vue-smart-loading-kit

Or with pnpm:

pnpm add vue-smart-loading-kit

Or with yarn:

yarn add vue-smart-loading-kit

Upgrading from 0.1: run npm install vue-smart-loading-kit@latest. Below 1.0, a ^0.1.0 range only matches 0.1.x, so npm update never reaches 0.2 or later, which added SmartLoader, v-skeleton and the rest. If your dev server then still reports a missing export, delete Vite's cache (node_modules/.cache, and .nuxt in Nuxt) and restart it.


🚀 Quick Start

Local registration

Import the components you need:

<script setup lang="ts">
import { Skeleton, Spinner } from 'vue-smart-loading-kit'
import 'vue-smart-loading-kit/style.css'
</script>

<template>
  <Skeleton variant="text" />
  <Spinner variant="dots" />
</template>

Global registration

You can also install the library globally:

import { createApp } from 'vue'
import App from './App.vue'
import VueSmartLoadingKit from 'vue-smart-loading-kit'
import 'vue-smart-loading-kit/style.css'

const app = createApp(App)

app.use(VueSmartLoadingKit)

app.mount('#app')

After global registration, the components can be used directly:

<template>
  <Skeleton variant="text" />
  <Spinner variant="dots" />
</template>

Nuxt

Register the plugin in a Nuxt plugin. Everything is SSR-safe:

// plugins/loading-kit.ts
import VueSmartLoadingKit from 'vue-smart-loading-kit'
import 'vue-smart-loading-kit/style.css'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(VueSmartLoadingKit)
})

To have PageProgress follow page navigation, pass the router: nuxtApp.vueApp.use(VueSmartLoadingKit, { router: useRouter() }).


🧩 Skeleton

The Skeleton component provides predefined loading placeholders for common UI patterns.

Available Variants

| Variant | Description | | --------- | --------------------------- | | text | Text/content placeholder | | block | Generic rectangular block | | circle | Circular placeholder | | avatar | Circular avatar placeholder | | button | Button-shaped placeholder | | input | Input-shaped placeholder | | card | Card placeholder | | article | Article/content placeholder | | profile | Profile layout placeholder | | image | Image/media placeholder | | list | List-style placeholder | | grid | Grid-style placeholder | | table | Table-style placeholder |

Example

<template>
  <div class="example">
    <Skeleton variant="text" />
    <Skeleton variant="avatar" />
    <Skeleton variant="card" />
    <Skeleton variant="image" />
  </div>
</template>

🎨 Customization

Skeleton components can be customized through their available props.

Size

Numeric values are automatically converted to pixels:

<Skeleton variant="avatar" :size="72" />

You can also provide CSS size values:

<Skeleton variant="avatar" size="4rem" />

Radius

<Skeleton variant="avatar" :radius="12" />

Or use any valid CSS value:

<Skeleton variant="avatar" radius="50%" />

Animation

Skeleton animations can be changed or disabled:

<Skeleton variant="avatar" animation="pulse" />

Disable animation:

<Skeleton variant="avatar" animation="none" />

Custom colors

<Skeleton
  variant="avatar"
  color="#7c3aed"
  highlight="#41B780"
/>

Outlined mode

<Skeleton
  variant="avatar"
  outlined
/>

Pass an object to control the border's thickness and style:

<Skeleton
  variant="card"
  :outlined="{ width: 2, style: 'dashed' }"
/>

| Field | Type | Default | Notes | | --------- | ------------------ | --------- | -------------------------------------------------- | | width | number \| string | 1 | Numbers are px; strings accept any CSS length. | | style | string | "solid" | Any CSS border-style (dashed, dotted, ...). | | enabled | boolean | true | Set false to turn the outline off from a config. |

The outline applies to every shape of composite variants (card, table, list, ...). The border is drawn inside each shape, so a width close to a shape's height — such as the 10px text lines in profile — fills the shape completely.


⏳ Spinner

Spinner provides lightweight animated loading indicators for smaller loading states.

Available Variants

| Variant | Description | | ------------ | -------------------- | | circle | Spinning circle (default) | | dots | Dots loader | | pulse | Pulse loader | | pulse-dots | Pulsing dots loader | | orbit | Orbit-style loader | | orbit-dots | Orbiting dots loader | | ring | Ring loader | | bars | Animated bars loader | | arc | Material-style arc; also a progress indicator via value |

Example

<template>
  <Spinner variant="dots" />
  <Spinner variant="pulse" />
  <Spinner variant="arc" />
</template>

Spinners use the surrounding text color by default (currentColor), so they fit buttons and dark mode without any configuration:

<button :disabled="saving">
  <Spinner v-if="saving" variant="arc" size="1em" :thickness="2" />
  Save
</button>

Progress

Pass value (0–100) to arc to show determinate progress, for example during an upload:

<Spinner variant="arc" :value="uploadPercent" track label="Uploading file" />

With a label, it is announced to screen readers as a progressbar with the current percentage. Other variants ignore value and log a warning in development. For a linear bar, see ProgressBar below.


📶 ProgressBar

A linear progress indicator. It fills to value (0–100), or shows a sliding segment when value is left out.

<!-- determinate: e.g. a file upload -->
<ProgressBar :value="uploadPercent" label="Uploading report.pdf" />

<!-- indeterminate: e.g. a thin bar at the top of the page -->
<ProgressBar :thickness="3" :track="false" />

| Prop | Type | Default | Notes | | ----------- | ------------------- | ---------------- | ------------------------------------------------------------ | | value | number | — | 0–100. Out-of-range values are clamped; omit for indeterminate. | | color | string | "currentColor" | Inherits the text color unless set. | | track | boolean \| string | true | The background rail. A string sets its color. | | thickness | number | 4 | Bar height in px. The bar always spans the container's width. | | speed | number | 1 | Speed of the indeterminate animation. | | label | string | — | Announced as a progressbar with its percentage, or as a live status when indeterminate. |

Like the spinners, it runs at half speed under prefers-reduced-motion.


🧠 SmartLoader

SmartLoader wraps content and decides when a loader is worth showing:

  • a load shorter than delay never shows a loader, so fast requests don't flash;
  • once shown, the loader stays for at least minDuration, so it never just blinks;
  • if loading restarts while the loader is still up, it simply stays;
  • while the loader stands in for the content, it keeps the content's height, so the page below doesn't jump;
  • it has a built-in error state with a retry button.
<SmartLoader :loading="loading" :skeleton="{ variant: 'list' }">
  <UserList :users="users" />
</SmartLoader>

Modes

| Mode | While loading | Good for | | --------------------- | ------------------------------------------------------------------------------ | --------------------------------- | | replace (default) | Swaps the content for a skeleton | First load, when there is no data | | overlay | Keeps the content, dimmed and inert, and centers a spinner over it | Refreshing data, submitting forms | | skeletonize | Keeps the content and turns it into a matching skeleton with v-skeleton | First load with placeholder data, refreshing |

<SmartLoader :loading="saving" mode="overlay" :spinner="{ variant: 'arc' }">
  <ProfileForm />
</SmartLoader>

SmartLoader or v-skeleton?

v-skeleton only decides what loading looks like: it turns real content into a skeleton, with delay and minDuration. SmartLoader manages a whole loading state: when to show a loader, the error with a retry button, the screen-reader announcement and the content's height. Its skeletonize mode uses v-skeleton inside.

| You want | Use | | ------------------------------------------------------------- | ------------------------------------------ | | A small piece (a card, a row) and errors handled elsewhere | v-skeleton | | A section that loads from an API, with an error and retry | <SmartLoader mode="skeletonize"> | | A skeleton but no placeholder data to render | <SmartLoader> (replace, the default) | | To keep showing the old data while it updates | <SmartLoader mode="overlay"> |

Props

| Prop | Type | Default | Notes | | ------------- | ------------------------ | ----------- | --------------------------------------------------------------------- | | loading | boolean | required | | | mode | "replace" \| "overlay" \| "skeletonize" | "replace" | | | delay | number | 200 | ms a load must last before the loader appears. 0 shows it at once. | | minDuration | number | 500 | ms the loader stays once shown. | | skeleton | Skeleton props | 3 text lines | Default loader in replace mode. | | spinner | Spinner props | arc | Default loader in overlay mode. | | label | string | — | Announced to screen readers while the loader is visible. | | error | unknown | — | Any truthy value (true, an Error, a message) shows the error state. | | preserveHeight | boolean | true | replace mode: keep the content's height while the loader is shown. |

Use the #loader slot to render your own loader instead. The wrapper gets aria-busy="true" as soon as loading starts, even before the loader appears.

Errors

<SmartLoader :loading="loading" :error="error" @retry="loadUsers">
  <UserList :users="users" />
</SmartLoader>

When error is set (and no loader is showing), the content is replaced by a short message and a Try again button that emits retry. It is announced with role="alert". During a retry the error stays on screen until the loader appears, so nothing flashes in between. In overlay mode the message sits over the dimmed content.

If error is a string, it is shown as the message, so you can write it for your users (and in their language):

<SmartLoader :loading="loading" :error="failed ? 'Couldn\'t load your team. Check your connection.' : null" @retry="loadTeam">

Any other value (true, an Error) shows the generic "Something went wrong.", because an Error's message is usually technical. For your own UI, use the #error slot. It receives the error and a retry function:

<template #error="{ error, retry }">
  <p>Could not load users: {{ error.message }}</p>
  <button @click="retry">Retry</button>
</template>

useDelayedLoading

The same timing logic is available as a composable, for loaders you render yourself:

import { useDelayedLoading } from 'vue-smart-loading-kit'

const showLoader = useDelayedLoading(loading, { delay: 200, minDuration: 500 })

loading and both options may be refs or getters.


🎯 Usage Examples

Loading a user profile

<template>
  <div v-if="loading">
    <Skeleton variant="profile" />
  </div>

  <div v-else>
    <h2>{{ user.name }}</h2>
    <p>{{ user.bio }}</p>
  </div>
</template>

Loading a card list

<template>
  <div v-if="loading">
    <Skeleton variant="grid" />
  </div>

  <div v-else>
    <!-- Your content -->
  </div>
</template>

Button loading state

<button :disabled="loading">
  <Spinner v-if="loading" variant="dots" />
  <span v-else>Submit</span>
</button>

🦴 Skeletonize

v-skeleton turns the content you already render into a skeleton with exactly the same layout: text becomes one bar per line, images and buttons become blocks, and container borders and spacing stay. Nothing is hand-built and nothing moves, because the same elements stay in place.

<UserCard v-skeleton="loading" :user="user" />
<div v-skeleton="{ loading, delay: 0, minDuration: 500 }">...</div>

It is CSS only: the directive toggles a class, a few attributes and CSS variables on the element. The DOM is never changed, so it is safe with any Vue content.

Setup: the plugin registers it (app.use(VueSmartLoadingKit) plus import 'vue-smart-loading-kit/style.css'). Without the plugin, import it in <script setup>, where an import named vSkeleton is available as v-skeleton:

import { vSkeleton } from 'vue-smart-loading-kit'

Render something to skeletonize. v-skeleton turns what is already on the page into a skeleton, so an empty list has nothing to turn into bars. On the first load, render placeholder data with the same shape as the real data:

<script setup lang="ts">
import { onMounted, ref } from 'vue'

// Fake rows with realistic text lengths: they decide the skeleton's shape
const placeholder = [1, 2, 3].map((id) => ({ id: -id, name: 'Placeholder name', bio: 'A short bio, about as long as a real one.' }))

const users = ref(placeholder)
const loading = ref(true)

onMounted(async () => {
  try {
    users.value = await fetch('/api/users').then((r) => r.json())
  } catch {
    users.value = []
  } finally {
    loading.value = false
  }
})
</script>

<template>
  <ul v-skeleton="{ loading, delay: 0 }">
    <li v-for="u in users" :key="u.id">
      <strong>{{ u.name }}</strong>
      <p>{{ u.bio }}</p>
    </li>
  </ul>
</template>

The live demo has a step-by-step guide on its Skeletonize page.

Two ways to use it:

  • Refreshing real data: keep the default delay (200ms), so quick refreshes don't flash.
  • Placeholder data on first load: render the component with fake data and use delay: 0, so the fake content is never visible. With SSR it is skeletonized in the server HTML already.

While skeletonized, the element is inert and aria-hidden (fake or stale content is neither read nor clickable), and aria-busy is set as soon as loading starts. Any aria-hidden, aria-busy or inert you set yourself is restored afterwards. Colors and animation come from the skeleton section of the global config, and the timing defaults from smartLoader.

Per-element control

| Attribute | Effect | | ------------------------- | ----------------------------------------------------------------------- | | data-skeleton="ignore" | The element and its children stay as they are (e.g. a static heading). | | data-skeleton="block" | The whole element becomes one solid block (charts, initials avatars, badges). | | data-skeleton="text" | Forces line bars where text and elements are mixed. |

With SmartLoader

mode="skeletonize" keeps SmartLoader's timing, error state and label announcement, and skeletonizes its content:

<SmartLoader :loading="loading" mode="skeletonize" label="Loading users">
  <UserList :users="users" />
</SmartLoader>

Limitations

  • The last line of a wrapped paragraph gets a full-width bar, because CSS cannot know its real length.
  • A few characters on a colored shape (initials avatars, badges) look like a line of text to CSS and would get stripes. Mark them data-skeleton="block".
  • video, canvas and iframe are hidden but keep their space. Wrap them in data-skeleton="block" for a solid block.
  • A text block taller than its text (table cells in rows of uneven height, or flex/grid items stretched to their neighbors) gets bars over its whole height, because CSS can't know how many lines the text really has. Mark such cells data-skeleton="block" if that matters.
  • Requires evergreen browsers from late 2023 on, for the lh unit, :has() and mask-composite.

🧭 PageProgress

A thin bar pinned to the top of the page, like the ones on GitHub or YouTube. Place it once, then let it follow your router, your own tasks, or both:

// main.ts
app.use(VueSmartLoadingKit, { router }) // follows every navigation
<!-- App.vue -->
<PageProgress />
<RouterView />
// anywhere, for your own tasks (fetches, uploads, ...)
const progress = usePageProgress()
progress.start()
await fetchData()
progress.done()
  • Tasks shorter than delay (default 200ms) never show the bar, so fast or cached navigations don't flash.
  • The real duration is unknown, so the bar eases toward 90%. done() fills it to 100%, and then it fades out.
  • Parallel tasks are counted: the bar stays until the last started task is done.
  • A navigation that is redirected counts once and only ends its own slot, so it never ends a task you started.
  • vue-router is not a dependency. Only beforeEach, afterEach and onError are used.
  • Safe for SSR: nothing runs on the server.

| Prop | Type | Default | Notes | | ----------- | -------------------- | ---------------- | ------------------------------------------------ | | color | string | "currentColor" | | | thickness | number | 3 | Bar height in px. | | delay | number | 200 | ms a task must last before the bar appears. | | label | string | — | Announced as a progressbar while visible. | | router | router | — | Alternative to passing router to the plugin. |

usePageProgress() needs the plugin (app.use(VueSmartLoadingKit)). Each app gets its own progress, so SSR requests never share state.


⚙️ Global config

Set defaults for the whole app once. An explicit prop always wins, then the config, then the built-in default:

app.use(VueSmartLoadingKit, {
  skeleton: { color: '#7c3aed', animation: 'pulse' },
  spinner: { variant: 'arc', thickness: 3 },
  progressBar: { thickness: 3 },
  smartLoader: { delay: 300, minDuration: 600, skeleton: { variant: 'list' } },
  pageProgress: { color: '#7c3aed' },
})

| Section | Keys | | -------------- | --------------------------------------------------------------------- | | skeleton | color, highlight, animation, speed, angle, outlined | | spinner | variant, color, size, speed, thickness, track | | progressBar | color, thickness, track, speed | | smartLoader | mode, delay, minDuration, preserveHeight, skeleton, spinner | | pageProgress | color, thickness, delay, label |

Only appearance and behavior are configurable, not per-instance layout such as width or lines.

To override the config for part of the page, such as a dark section, or to configure the kit without the plugin (local imports), call provideLoadingConfig in a component's setup. It merges with the config above it:

import { provideLoadingConfig } from 'vue-smart-loading-kit'

provideLoadingConfig({ skeleton: { color: 'rgba(255, 255, 255, 0.12)' } })

🛠️ TypeScript

The library ships with TypeScript declarations out of the box, so component props and public APIs are typed automatically when used in TypeScript projects.

import { Skeleton, Spinner, ProgressBar, SmartLoader, PageProgress, usePageProgress, vSkeleton } from 'vue-smart-loading-kit'
import type { LoadingKitConfig, SkeletonVariantName, SpinnerProps } from 'vue-smart-loading-kit'

📚 API Overview

Skeleton

Common customization options include:

  • variant
  • size
  • radius
  • animation
  • speed
  • color
  • highlight
  • outlined

Example:

<Skeleton
  variant="avatar"
  :size="64"
  :radius="12"
  animation="shimmer"
  :speed="1"
  color="#e5e7eb"
  highlight="#f3f4f6"
  outlined
/>

Spinner

Spinner variants provide different loading animations while keeping the component simple to integrate into existing interfaces.

<Spinner variant="ring" :size="48" color="#7c3aed" :speed="1.5" :thickness="3" label="Loading" />

| Prop | Type | Default | Notes | | ----------- | ------------------- | ---------------- | ------------------------------------------------------------ | | variant | see table above | "circle" | | | size | number \| string | 40 | Every variant renders a size x size box. | | color | string | "currentColor" | Inherits the text color unless set. | | speed | number | 1 | | | thickness | number | 4 | Stroke width in px for circle, ring and arc. | | track | boolean \| string | variant default | Faint circle behind circle (on by default), ring, arc. A string sets its color. | | value | number | — | 0–100, arc only: turns it into a progress indicator. | | label | string | — | Makes the spinner announce itself to screen readers. |

size accepts a number (px) or any CSS length ("3rem", "50%"). When the user has prefers-reduced-motion: reduce enabled, spinners run at half speed instead of stopping, so the page never looks frozen.

The type prop and camelCase names (pulseDots, orbitDots) from v0.1.0 still work but are deprecated in favor of variant.


🏗️ Development

Clone the repository:

git clone https://github.com/navidjaberi/vue-smart-loading-kit.git
cd vue-smart-loading-kit

Install dependencies:

npm install

Start the interactive playground, which imports the components straight from src/ so edits show up live:

npm run dev

Every variant has its own controls and a copyable code snippet, plus a view that compares all variants side by side. The playground lives in playground/ and is not part of the published package. Every push to main deploys it to the live demo.

Run tests and the type check:

npm test
npm run typecheck
npm run test:browser   # visual rules in a real Chromium (run `npx playwright install chromium` once)

Build the library:

npm run build

🧪 Testing

1,400+ unit tests and 11 real-browser tests, run on Node 20 and 22 for every push and pull request. Coverage is 100% of lines (96% of branches), and CI fails if it drops below the thresholds in vitest.config.ts.

Coverage only shows that code ran. Mutation testing (Stryker) shows the tests would catch a bug: it plants small bugs in the source (flips a condition, changes a default, drops a call) and reruns the tests against each one. The mutation score is over 90% (live in the badge above): over 90% of those planted bugs make a test fail. The surviving gaps it found led to new tests and two real bug fixes.

Both reports are published with the demo: coverage and mutation.

Loading UI fails in ways a plain "does it render" test misses: a spinner that flickers, a skeleton that shifts the layout, a hydration mismatch. The suite targets those directly:

| Layer | What it proves | Where | | --- | --- | --- | | Unit (Vitest + jsdom) | Every variant and prop, through the public Skeleton / Spinner components | tests/Skeleton, tests/Spinner | | Timing (fake timers) | delay and minDuration never flicker: every value the loader takes is recorded, so a single wrong frame fails the test | tests/utils/useDelayedLoading.test.ts, tests/PageProgress.test.ts | | Real browser (Vitest Browser Mode + Playwright Chromium) | v-skeleton moves or resizes no element, hides text, and keeps borders. jsdom has no layout, so these run in Chromium | tests/browser | | SSR and hydration | Server rendering works without a DOM, and the client hydrates the server HTML without a mismatch | tests/ssr.test.ts, tests/hydration.test.ts | | Geometry | The shimmer covers the shape cleanly at all 360 angles, with no hard edges | tests/Skeleton/shimmer.test.ts | | Invariants | No color literals in spinner styles, every spinner is size x size, every global style is vslk- prefixed, no stray HTML attributes | tests/Spinner/consistency.test.ts, tests/css-isolation.test.ts | | Mutation testing (Stryker) | Every planted bug is caught by some test; survivors point at missing assertions | stryker.config.json | | Config precedence | An explicit prop (even false) beats the global config, which beats the built-in default | tests/config.test.ts |

Bug fixes start with a failing test.

npm test               # unit tests
npm run test:coverage  # unit tests with coverage and thresholds
npm run test:mutation  # mutation testing with Stryker (incremental after the first run)
npm run test:browser   # real-browser tests (run `npx playwright install chromium` once)
npm run typecheck      # vue-tsc over src and tests

📦 Build

The package is built with Vite and provides both ESM and UMD/CommonJS-compatible outputs for broader ecosystem compatibility.

npm run build

Generated files are placed in the dist directory.


🤝 Contributing

Contributions, bug reports, feature requests, and pull requests are welcome.

Before submitting a pull request, please make sure your changes pass the test suite:

npm test

📄 License

MIT © Navid Jaberi


⭐ Support

If vue-smart-loading-kit is useful for your project, consider giving the repository a star on GitHub.

Made with ❤️ for the Vue community.