Skeleton

A placeholder block shown while content loads, with a pulse or shimmer animation.

Install

If this is the first component in the project, set up once. This writes ermine.json and copies the theme stylesheet. The installation guide covers Vue, Nuxt and Astro.

npx ermineui init
pnpm dlx ermineui init
yarn dlx ermineui init
bunx ermineui init

Then add the component:

npx ermineui add skeleton
pnpm dlx ermineui add skeleton
yarn dlx ermineui add skeleton
bunx ermineui add skeleton

That copies 2 files into src/components/ui/skeleton/. Files you already have are kept.

Use

import { Skeleton } from '@/components/ui/skeleton'
<div :aria-busy="loading">
  <template v-if="loading">
    <Skeleton circle style="height: 2.5rem" />
    <Skeleton style="width: 60%" />
    <Skeleton animation="shimmer" style="height: 8rem" />
  </template>
  <Profile v-else :user="user" />
</div>

In Nuxt, with the auto-import setting, the tags work in templates without the import. In Astro, use it inside a Vue island.

What gets copied

FileWhere it goes
Skeleton.vuesrc/components/ui/skeleton/Skeleton.vue
index.tssrc/components/ui/skeleton/index.ts

The folders are the suggested ones. ermine.json says where yours are.

Source

This is exactly what the command copies. To install by hand, create each file below and the helpers listed above.

Skeleton.vue 74 lines
<script setup lang="ts">
withDefaults(
  defineProps<{
    animation?: 'pulse' | 'shimmer' | 'none'
    /** A round placeholder, for avatars and icons. Set one of width or height; the other follows. */
    circle?: boolean
  }>(),
  { animation: 'pulse' },
)
</script>

<template>
  <!-- Hidden from screen readers: mark the region that is loading with aria-busy="true" instead. -->
  <div class="er-skeleton" aria-hidden="true" :data-animation="animation" :data-circle="circle ? '' : undefined" />
</template>

<style>
@layer components {
  /* One line of text by default. Size it with width / height, in a style attribute or your own class. */
  .er-skeleton {
    display: block;
    width: 100%;
    height: 1rem;
    border-radius: var(--er-radius-sm);
    background-color: var(--er-skeleton, color-mix(in oklab, var(--er-foreground) 10%, transparent));
  }

  .er-skeleton[data-circle] {
    flex-shrink: 0;
    width: auto;
    aspect-ratio: 1;
    border-radius: 50%;
  }

  .er-skeleton[data-animation='pulse'] {
    animation: er-skeleton-pulse 1.6s ease-in-out infinite;
  }

  .er-skeleton[data-animation='shimmer'] {
    background-image: linear-gradient(
      100deg,
      transparent 30%,
      var(--er-skeleton-highlight, light-dark(oklch(1 0 0 / 65%), oklch(1 0 0 / 9%))) 50%,
      transparent 70%
    );
    background-size: 250% 100%;
    background-repeat: no-repeat;
    animation: er-skeleton-shimmer 1.6s linear infinite;
  }

  @keyframes er-skeleton-pulse {
    50% {
      opacity: 0.45;
    }
  }

  @keyframes er-skeleton-shimmer {
    from {
      background-position: 150% 0;
    }
    to {
      background-position: -50% 0;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .er-skeleton {
      animation: none !important;
      background-image: none !important;
    }
  }
}
</style>
index.ts 3 lines
/** Skeleton — a placeholder block shown while content loads, with a pulse or shimmer animation. */
export { default as Skeleton } from './Skeleton.vue'