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

auto-skelly

v1.0.0

Published

Framework-agnostic skeleton loading placeholders. Auto-generates animated skeletons from your existing markup to improve perceived load time.

Readme

auto-skelly

Framework-agnostic skeleton loading placeholders, generated automatically from your existing markup.

npm version CI License: MIT

Auto Skelly demo header

Live demo →

Why

Users perceive pages as faster when something meaningful appears immediately, even before real data has arrived. Skeleton screens — animated placeholders shaped like the content that's loading — reduce perceived latency and avoid the layout jank of spinners or blank space. Auto Skelly generates those placeholders directly from your markup, so you don't hand-build a skeleton for every view.

Install

npm i auto-skelly

Or drop it in with a script tag — data-skelly-auto skellifies the page automatically on DOMContentLoaded:

<script src="https://unpkg.com/auto-skelly" data-skelly-auto></script>

Quick start

<div class="skelly-text">Order #1204 — Delivered</div>
<img class="skelly-image" src="/receipt.png" alt="Receipt" />
<button class="skelly-button">View details</button>

<script type="module">
  import { AutoSkelly } from "auto-skelly";

  const skelly = new AutoSkelly();
  await skelly.while(fetchOrder());
</script>

while(promise) is the shortest correct way to wrap an async load: it applies the skeleton, awaits promise, and always removes it again — even if the promise rejects — resolving (or rethrowing) with the promise's own value.

For anything that isn't a single promise (event-driven loading, multiple in-flight requests), call apply()/remove() directly:

const skelly = new AutoSkelly();
skelly.apply();

fetchOrder().then(() => {
  skelly.remove(); // originals reappear exactly as they were
});

Originals are hidden, not destroyed — remove() (or while()'s automatic cleanup) brings back the real content untouched.

API reference

new AutoSkelly(options?)

| Option | Type | Default | Description | | --- | --- | --- | --- | | color | string | "auto" | Placeholder background color. "auto" writes no inline custom property, so the injected stylesheet controls color — including a prefers-color-scheme: dark default. | | highlightColor | string | "auto" | Highlight color for the shimmer sweep. Same "auto" behavior as color. | | animation | "pulse" \| "extraPulse" \| "gradient" \| "shimmer" \| "none" | "pulse" | Placeholder animation. | | root | ParentNode | document | Root to search/restore within by default. | | delay | number | 0 | Milliseconds to wait before painting placeholders. 0 is synchronous. | | minDuration | number | 0 | Minimum milliseconds a painted placeholder stays visible before an early remove() can take it away. | | fade | number | 0 | Milliseconds to fade a placeholder's opacity to 0 before restoring the original element. 0 swaps instantly. | | observe | boolean | false | Watch root with a MutationObserver and skellify matching elements added after apply(). |

Methods

  • apply(root?: ParentNode): () => void — Finds matching elements under root (default: the instance's configured root, or document), replaces each with a sized placeholder, and hides the original in place. Returns an idempotent disposer scoped to this call — calling it removes only what this call skellified, and calling it more than once is a no-op.
  • remove(root?: ParentNode): void — Restores original elements and removes their placeholders, honoring minDuration/fade. Pass root to restore only elements contained within it; omit it to restore everything this instance currently has applied.
  • while<T>(promise: Promise<T>, root?: ParentNode): Promise<T> — Applies, awaits promise, then always removes — even if it rejects — and resolves (or rethrows) with the promise's own result.
  • toggle(on: boolean, root?: ParentNode): void — Sugar for on ? apply(root) : remove(root).
  • setTheme(theme: { color?: string; highlightColor?: string; animation?: SkellyAnimation }): void — Updates color/highlight color/animation on every placeholder this instance currently has applied. Per-element data-skelly-* overrides still take precedence. Pass "auto" to hand a color back to the stylesheet.
  • active — Read-only getter. true while a paint is pending (mid-delay) or currently painted; false once removal has committed, even mid-fade.

Shape classes

  • .skelly-text — a bar sized to the measured line-height; content taller than two line-heights becomes stacked bars, with a shorter final line.
  • .skelly-image — a rectangle sized to the element, falling back to an <img>'s width/height attributes, then a 16:9 aspect ratio.
  • .skelly-circle — a circle sized to the larger of the element's measured width/height.
  • .skelly-button — a rectangle sized to the element's outer width/height.

Sizing also copies each element's computed border-radius (falling back to 5px for text/buttons, 50% for circles, 0 for images) and margins.

Any element can also declare its shape with data-skelly-shape="text" | "image" | "circle" | "button" instead of (or alongside) these classes — handy when you can't add a class. See the next section for the full list of data-skelly-* attributes.

Per-element overrides (data-skelly-*)

Any matching element can override the instance-wide options, or the measured/default sizing, with data-skelly-* attributes:

| Attribute | Description | | --- | --- | | data-skelly-shape | "text" \| "image" \| "circle" \| "button" — declare the shape without a skelly-* class. | | data-skelly-width | Placeholder width (any CSS length, e.g. "200px"). | | data-skelly-height | Placeholder height. | | data-skelly-radius | Placeholder border-radius. | | data-skelly-lines | Integer ≥ 1. On .skelly-text, forces the number of stacked bars, overriding the height-based multiline detection. | | data-skelly-animation | "pulse" \| "extraPulse" \| "gradient" \| "shimmer" \| "none" — overrides the instance animation for this element. | | data-skelly-color | Overrides the instance/CSS color for this element. Accepts "auto". | | data-skelly-highlight-color | Overrides the shimmer highlight color for this element. Accepts "auto". | | data-skelly-count | Integer > 1. Clones this element's placeholder N times. | | data-skelly-ignore | Present (any value) — skip this element entirely; it's left untouched, no placeholder is inserted. |

Precedence, most to least specific: data-skelly-* attribute → constructor option (where one exists, e.g. color, animation) → value measured from the DOM → the shape's built-in default. Not every property passes through every tier — lines and count, for example, have no constructor-level equivalent.

Skellifying content that doesn't exist yet

data-skelly-lines also works on an empty element — the common case in SPA/React apps, where the real content hasn't been fetched into the DOM yet and there's nothing to measure:

<div class="skelly-text" data-skelly-lines="3"></div>

With no rect to measure, an unforced placeholder would fall back to a single 1em bar. Forcing lines builds the full multi-bar skeleton (with the usual shorter final line) up front.

Standing in for a list

data-skelly-count repeats one template element's placeholder N times, so you don't need N real elements in the DOM to skellify a list:

<div class="skelly-text" data-skelly-count="5"></div>

This inserts 5 placeholder bars before the single template element, then hides the template as usual.

Theming

Dark mode works out of the box. color and highlightColor default to "auto", which writes no inline custom property at all — the injected stylesheet governs appearance, including a prefers-color-scheme: dark default for both. Set an explicit color to opt out.

Three ways to control appearance, in increasing order of granularity:

  1. Constructor options — set color/highlightColor/animation once, at construction.
  2. setTheme({ color?, highlightColor?, animation? }) — change color/highlight color/animation on an already-applied instance; every visible placeholder updates immediately. Pass "auto" to return a color to CSS control.
  3. CSS custom properties — set these on any ancestor (or :root) to override appearance without touching JS:
    • --skelly-color — placeholder background color.
    • --skelly-highlight-color — the shimmer sweep's highlight color.
    • --skelly-duration — animation duration (default varies by animation: 2s for pulse/extraPulse, 5s for gradient, 1.5s for shimmer).
    • --skelly-gradient — the gradient animation's background-image.

Per-element data-skelly-* attributes take precedence over both constructor options and setTheme() — see Per-element overrides above.

Animations:

  • pulse (default) — opacity fade.
  • extraPulse — scale + box-shadow pulse.
  • gradient — animated diagonal gradient sweep, themed via --skelly-gradient.
  • shimmer — a light sweep across the placeholder, themed via --skelly-highlight-color.
  • none — static placeholder, no animation.

Timing: delay, minDuration, and fade

All three default to 0, so out of the box apply()/remove() are synchronous — no delay, no fade, identical to not setting them at all.

  • delay — wait this many ms before painting placeholders. If the real content is ready (you call remove(), or the while() promise settles) before the delay elapses, no placeholder is ever painted — fast loads never flash a skeleton.
  • minDuration — once a placeholder has painted, keep it visible for at least this many ms even if remove() is called earlier. Stops a skeleton from flickering in and immediately back out on a load that turns out to be fast.
  • fade — once removal actually happens (after any minDuration wait), fade the placeholder's opacity to 0 over this many ms before restoring the original element, instead of swapping instantly.

A good starting point:

const skelly = new AutoSkelly({ delay: 200, minDuration: 400 });

Loads under 200ms show no skeleton at all; any skeleton that does appear stays for at least 400ms, long enough to read as intentional rather than a flicker.

Watching for new elements: observe

By default, apply() scans once. Pass observe: true to also watch root with a MutationObserver and automatically skellify any matching elements added later — useful for infinite-scroll lists or content injected after the initial render:

const skelly = new AutoSkelly({ observe: true });
const dispose = skelly.apply(listEl);

// Elements matching .skelly-* added under listEl after this point
// are picked up automatically.

Call the returned disposer (or remove()) to stop watching and restore everything the call painted.

Framework recipes

React

apply() returns an idempotent disposer scoped to that call, which is exactly what a React effect wants back — it fires on unmount and restores the originals automatically:

import { useEffect, useRef } from "react";
import { AutoSkelly } from "auto-skelly";

function OrderCard({ children }) {
  const rootRef = useRef(null);
  const skelly = useRef(new AutoSkelly());

  useEffect(() => skelly.current.apply(rootRef.current), []);

  return <div ref={rootRef}>{children}</div>;
}

Vue 3

<script setup>
import { ref, onMounted, watchEffect } from "vue";
import { AutoSkelly } from "auto-skelly";

const rootEl = ref(null);
const isLoading = ref(true);
const skelly = new AutoSkelly();

onMounted(() => {
  watchEffect(() => {
    isLoading.value ? skelly.apply(rootEl.value) : skelly.remove(rootEl.value);
  });
});
</script>

<template><div ref="rootEl"><!-- skelly-* markup --></div></template>

Plain script tag

<!-- Auto-applies on DOMContentLoaded -->
<script src="https://unpkg.com/auto-skelly" data-skelly-auto></script>

Without data-skelly-auto, the script still exposes window.AutoSkelly for manual control:

<script src="https://unpkg.com/auto-skelly"></script>
<script>new window.AutoSkelly().apply();</script>

Accessibility

  • Placeholders are marked aria-hidden="true".
  • The parent of any skellified element gets aria-busy="true" for as long as it has active children, cleared automatically once they're all restored.
  • Animations only run under prefers-reduced-motion: no-preference; placeholders render static otherwise.

Migrating from v0

v0 was a jQuery-based, unpublished prototype distributed by copying autoskelly.js/autoskelly.css into your project. v1 is a zero-dependency TypeScript rewrite published to npm.

| v0 | v1 | | --- | --- | | setSkelly(color, animation) | new AutoSkelly({ color, animation }).apply() | | changeSkellyColor(color) | setTheme({ color }) | | changeSkellyAnimation("standard") | setTheme({ animation: "pulse" }) | | changeSkellyAnimation("bigPulse") | setTheme({ animation: "extraPulse" }) | | activateSkelly(true) / activateSkelly(false) | apply() / remove() |

The most important behavioral change: v0 permanently destroyed skellified elements via jQuery's replaceWith. v1 hides originals and restores them exactly on remove() — skellification is now reversible.

Contributing

See CONTRIBUTING.md.

License

MIT © Alex Gordienko