DEV Community

Riaz Ahmed
Riaz Ahmed

Posted on

Stop writing skeleton shapes by hand — auto-skeleton does it for you

Skeleton screens are one of the best things you can do for perceived performance. But maintaining a separate set of hand-drawn rectangles and circles that mirror your real UI? That part is a grind — and it silently breaks every time your layout changes.

auto-skeleton fixes this. It measures your actual rendered DOM at runtime using getBoundingClientRect and generates pixel-accurate skeletons automatically. No shape definitions. No CLI snapshots. No config files. Just wrap your component and you're done.


The problem with existing solutions

Most skeleton libraries put the burden on you. You describe the skeleton — "three lines, one circle, two rectangles" — and hope it still matches your UI six sprints from now.

Feature Manual shapes CLI snapshot tools auto-skeleton
Zero config Partial
No build step
Always in sync with real UI Partial
Adapts to dynamic content
Cached for instant reloads

The core issue with manual approaches is that the skeleton is a second source of truth. auto-skeleton eliminates that entirely.


How it works

Rather than asking you to describe your UI twice, auto-skeleton reads it once — at runtime, on the real DOM.

  1. On the first non-loading render, the DOM inside <AutoSkeleton> is scanned using a TreeWalker
  2. Each visible element is measured with getBoundingClientRect and classified as a rect, circle, or text block
  3. Bone positions are stored in memory and sessionStorage keyed by id + viewport width
  4. When loading switches back to true, an absolutely-positioned overlay renders the cached bones instantly
  5. The real content is hidden (visibility: hidden) so layout is preserved during loading

React quick start

npm install @auto-skeleton/react
Enter fullscreen mode Exit fullscreen mode
import { AutoSkeleton } from "@auto-skeleton/react";
import "@auto-skeleton/react/styles.css";

function UserProfile({ userId }) {
  const { data, isLoading } = useUser(userId);

  return (
    <AutoSkeleton id="user-profile" loading={isLoading}>
      <div className="profile">
        <img src={data?.avatar} data-skeleton-shape="circle" />
        <h2 data-skeleton-lines="1">{data?.name}</h2>
        <p data-skeleton-lines="3">{data?.bio}</p>
      </div>
    </AutoSkeleton>
  );
}
Enter fullscreen mode Exit fullscreen mode

data-skeleton-shape="circle" tells the scanner to render the avatar as a circle. data-skeleton-lines="3" splits the paragraph into three stacked text lines. Both are optional — the scanner makes reasonable guesses without them.


Vue 3 quick start

npm install @auto-skeleton/vue
Enter fullscreen mode Exit fullscreen mode
<script setup>
import { ref } from "vue";
import { AutoSkeleton } from "@auto-skeleton/vue";
import "@auto-skeleton/vue/styles.css";

const loading = ref(true);
</script>

<template>
  <AutoSkeleton id="user-profile" :loading="loading">
    <UserProfileCard />
  </AutoSkeleton>
</template>
Enter fullscreen mode Exit fullscreen mode

No changes required to UserProfileCard itself — the wrapper handles everything.


Lit / Web Components

npm install @auto-skeleton/lit lit
Enter fullscreen mode Exit fullscreen mode
import "@auto-skeleton/lit";
Enter fullscreen mode Exit fullscreen mode
<auto-skeleton skeleton-id="feed" .loading=${this.isLoading}>
  <feed-list></feed-list>
</auto-skeleton>
Enter fullscreen mode Exit fullscreen mode

Works natively in any framework — or no framework at all. The scanner crosses Shadow DOM boundaries automatically, so nested Lit components are discovered without extra configuration.


Options

Option Type Default Description
animation `"wave" \ "pulse" \ "none"`
debug boolean false Outline detected bones with dashed borders
cache boolean true Cache measurements after first scan
minSize number 8 Minimum element size (px) to include
watchDebounceMs number 120 Debounce delay for layout change re-scans
ignoreSelectors string[] [] CSS selectors for elements to skip

Data attributes

Fine-tune individual elements without changing render logic:

Attribute Effect
data-skeleton-ignore Skip this element entirely
data-skeleton-shape="circle" Force a circular bone
data-skeleton-lines="N" Split into N stacked text-line bars
data-skeleton-container Scan children individually instead of as one block

Theming

Two CSS custom properties control the entire look:

:root {
  --as-base: #e4e4e7;
  --as-highlight: rgba(255, 255, 255, 0.9);
}

.dark {
  --as-base: #27272a;
  --as-highlight: rgba(255, 255, 255, 0.05);
}
Enter fullscreen mode Exit fullscreen mode

--as-base is the bone colour. --as-highlight is the shimmer that sweeps across during the wave animation.


Why this matters beyond convenience

The maintenance cost of manual skeletons is easy to underestimate. It's not just the initial setup — it's every subsequent layout change. Your designer widens the avatar. You add a badge. The bio gets a second paragraph. Each change requires you to remember to update the skeleton too. Most teams don't. The result is skeletons that subtly diverge from the real UI, undermining the perceived-performance benefit they were supposed to deliver.

auto-skeleton makes this a non-issue. The skeleton is derived from the real UI, so it stays correct by construction. You only intervene with data attributes when you want to fine-tune — not to keep things from drifting.

This also makes it significantly faster to add loading states to new components. You don't need to design the skeleton separately — you just wrap the component and move on.


Try it

All packages are at v0.0.7. If you've been putting off skeleton loaders because of the setup cost — this is the week to change that. Drop a comment if you run into anything, and a ⭐ on GitHub goes a long way at this stage.

Top comments (0)