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 initpnpm dlx ermineui inityarn dlx ermineui initbunx ermineui initThen add the component:
npx ermineui add skeletonpnpm dlx ermineui add skeletonyarn dlx ermineui add skeletonbunx ermineui add skeletonThat 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
| File | Where it goes |
|---|---|
Skeleton.vue | src/components/ui/skeleton/Skeleton.vue |
index.ts | src/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'