Skeleton
skeletonuiAnimated placeholder rectangles for loading states — drop one in shape of the content that’s about to render. Variants for text lines, avatars, rounded rectangles, and circles.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/skeleton.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/skeleton.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/skeleton.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/skeleton.jsonManifest:
https://uipkge.dev/r/svelte/skeleton.jsonInstalls to:src/lib/components/ui/skeleton/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'text''chip''chip-icon''article''avatar''avatar-small''avatar-large''heading''heading-medium''heading-small''image''image-small''image-large''card''card-avatar''actions''table''table-row''button''button-icon''badge''tab''date-picker''list-item''list-item-two-line''list-item-three-line' | text | optional |
width | string | — | optional |
height | string | — | optional |
loading | boolean | true | optional |
ref | HTMLDivElement | null | — | optional |
npm dependencies
Used by
Files installed (6)
src/lib/components/ui/skeleton/Skeleton.svelte2.2 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> { variant?: 'rectangular' | 'rounded' | 'circular' | 'text' | 'avatar' | 'image' | 'card' | 'table-row' width?: string height?: string loading?: boolean ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' const variantClasses: Record<NonNullable<SkeletonProps['variant']>, string> = { rectangular: '', rounded: 'rounded-md', circular: 'rounded-full', text: 'rounded h-4 w-full', avatar: 'rounded-full size-10', image: 'rounded-lg size-24', card: 'rounded-xl size-full min-h-30', 'table-row': 'rounded h-10 w-full', } let { class: className, variant = 'rectangular', width, height, loading = true, children, ref = $bindable(null), ...restProps }: SkeletonProps = $props() const variantStyle = $derived( width || height ? `${width ? `width:${width};` : ''}${height ? `height:${height};` : ''}` : undefined, ) </script> {#if loading} <div bind:this={ref} data-uipkge="" data-slot="skeleton" aria-hidden="true" class={cn('skeleton-shimmer', variantClasses[variant ?? 'rectangular'], className)} style={variantStyle} {...restProps} ></div> {:else} {@render children?.()} {/if} <style> /* Slow left-to-right gradient sweep. Per NN/g, slow shimmer reads as faster loading than a pulse. 1.8s loop is the documented sweet spot. */ .skeleton-shimmer { background: linear-gradient( 90deg, color-mix(in srgb, var(--muted) 100%, transparent) 0%, color-mix(in srgb, var(--muted) 60%, var(--foreground) 8%) 50%, color-mix(in srgb, var(--muted) 100%, transparent) 100% ); background-size: 200% 100%; animation: skeleton-shimmer 1.8s linear infinite; } @keyframes skeleton-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } } @media (prefers-reduced-motion: reduce) { .skeleton-shimmer { animation: none; background: var(--muted); } } </style>src/lib/components/ui/skeleton/SkeletonGroup.svelte0.5 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SkeletonGroupProps extends HTMLAttributes<HTMLElement> { tag?: string } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, tag = 'div', children, ...restProps }: SkeletonGroupProps = $props() </script> <svelte:element this={tag} data-uipkge="" data-slot="skeleton-group" class={cn('space-y-2', className)} {...restProps}> {@render children?.()} </svelte:element>src/lib/components/ui/skeleton/SkeletonLoader.svelte4.2 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' import type { SkeletonLoaderVariants } from './skeleton.variants' export interface SkeletonLoaderProps extends HTMLAttributes<HTMLDivElement> { variant?: SkeletonLoaderVariants['variant'] loading?: boolean rows?: number boilerplate?: boolean ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { skeletonLoaderVariants } from './skeleton.variants' let { class: className, variant = 'text', loading = true, rows = 1, boilerplate = false, children, ref = $bindable(null), ...restProps }: SkeletonLoaderProps = $props() const rowIndexes = $derived(Array.from({ length: rows }, (_, i) => i)) </script> <div bind:this={ref} data-uipkge="" data-slot="skeleton-loader" class="space-y-2" {...restProps}> {#if rows === 1} {#if loading} <div class={cn(skeletonLoaderVariants({ variant }), className)}></div> {:else} {@render children?.()} {/if} {:else if loading} {#if variant === 'article'} <div class={cn(skeletonLoaderVariants({ variant: 'heading' }), 'mb-4')}></div> <div class={cn(skeletonLoaderVariants({ variant: 'text' }), 'mb-2')}></div> <div class={cn(skeletonLoaderVariants({ variant: 'text' }), 'mb-2')}></div> <div class={cn(skeletonLoaderVariants({ variant: 'text' }), 'w-3/4')}></div> {:else if variant === 'card'} <div class={cn(skeletonLoaderVariants({ variant: 'image-large' }), 'mb-4')}></div> <div class={cn(skeletonLoaderVariants({ variant: 'heading-small' }), 'mb-2')}></div> <div class={cn(skeletonLoaderVariants({ variant: 'text' }), 'mb-2')}></div> <div class={cn(skeletonLoaderVariants({ variant: 'text' }), 'w-1/2')}></div> {:else if variant === 'card-avatar'} <div class="mb-4 flex items-center gap-4"> <div class={cn(skeletonLoaderVariants({ variant: 'avatar-large' }))}></div> <div class="flex-1 space-y-2"> <div class={cn(skeletonLoaderVariants({ variant: 'heading-small' }))}></div> <div class={cn(skeletonLoaderVariants({ variant: 'text' }), 'w-1/2')}></div> </div> </div> {:else if variant === 'actions'} <div class="flex gap-2"> <div class={cn(skeletonLoaderVariants({ variant: 'button' }))}></div> <div class={cn(skeletonLoaderVariants({ variant: 'button' }))}></div> </div> {:else if variant === 'table'} {#each rowIndexes as i (i)} <div class={cn(skeletonLoaderVariants({ variant: 'table-row' }), 'mb-2')}></div> {/each} {:else if variant === 'list-item'} {#each rowIndexes as i (i)} <div class="mb-2 flex items-center gap-3"> <div class={cn(skeletonLoaderVariants({ variant: 'avatar-small' }))}></div> <div class="flex-1"> <div class={cn(skeletonLoaderVariants({ variant: 'text' }))}></div> </div> </div> {/each} {:else if variant === 'list-item-two-line'} {#each rowIndexes as i (i)} <div class="mb-2 flex items-center gap-3"> <div class={cn(skeletonLoaderVariants({ variant: 'avatar' }))}></div> <div class="flex-1 space-y-2"> <div class={cn(skeletonLoaderVariants({ variant: 'text' }))}></div> <div class={cn(skeletonLoaderVariants({ variant: 'text' }), 'w-3/4')}></div> </div> </div> {/each} {:else if variant === 'list-item-three-line'} {#each rowIndexes as i (i)} <div class="mb-2 flex items-start gap-3"> <div class={cn(skeletonLoaderVariants({ variant: 'avatar' }))}></div> <div class="flex-1 space-y-2"> <div class={cn(skeletonLoaderVariants({ variant: 'text' }))}></div> <div class={cn(skeletonLoaderVariants({ variant: 'text' }))}></div> <div class={cn(skeletonLoaderVariants({ variant: 'text' }), 'w-2/3')}></div> </div> </div> {/each} {:else} {#each rowIndexes as i (i)} <div class={cn(skeletonLoaderVariants({ variant }), 'mb-2')}></div> {/each} {/if} {:else} {@render children?.()} {/if} {#if boilerplate && !loading} <div class="bg-muted/50 absolute inset-0"></div> {/if} </div>src/lib/components/ui/skeleton/SkeletonText.svelte1.7 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SkeletonTextProps extends HTMLAttributes<HTMLDivElement> { lines?: number lastLineWidth?: string firstLineWidth?: string ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, lines = 3, lastLineWidth = '80%', firstLineWidth = '100%', ref = $bindable(null), ...restProps }: SkeletonTextProps = $props() const lineWidths = $derived( Array.from({ length: lines }, (_, i) => { if (i === 0) return firstLineWidth if (i === lines - 1) return lastLineWidth return '100%' }), ) </script> <div bind:this={ref} data-uipkge="" data-slot="skeleton-text" aria-hidden="true" class={cn('space-y-2', className)} {...restProps} > {#each lineWidths as width, i (i)} <div class="skeleton-shimmer h-4 rounded" style="width:{width}"></div> {/each} </div> <style> /* Match Skeleton.svelte shimmer so text lines don't flash a different motion language. */ .skeleton-shimmer { background: linear-gradient( 90deg, color-mix(in srgb, var(--muted) 100%, transparent) 0%, color-mix(in srgb, var(--muted) 60%, var(--foreground) 8%) 50%, color-mix(in srgb, var(--muted) 100%, transparent) 100% ); background-size: 200% 100%; animation: skeleton-shimmer 1.8s linear infinite; } @keyframes skeleton-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } } @media (prefers-reduced-motion: reduce) { .skeleton-shimmer { animation: none; background: var(--muted); } } </style>src/lib/components/ui/skeleton/skeleton.variants.ts1.6 kB
import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than inline in the * SFC) so `SkeletonLoader.vue` can import them without the cva runtime * coupling that breaks SSR when several `<SkeletonLoader>` instances * render before the module graph fully resolves. Sibling pattern to * `card/card.variants.ts`. */ export const skeletonLoaderVariants = cva('bg-muted animate-pulse rounded', { variants: { variant: { text: 'h-4 w-full', chip: 'h-8 w-24 rounded-full', 'chip-icon': 'h-8 w-8 rounded-full', article: '', avatar: 'size-12 rounded-full', 'avatar-small': 'size-8 rounded-full', 'avatar-large': 'size-16 rounded-full', heading: 'h-6 w-3/4', 'heading-medium': 'h-5 w-1/2', 'heading-small': 'h-4 w-1/3', image: 'aspect-video w-full rounded-lg', 'image-small': 'h-32 w-32 rounded-lg', 'image-large': 'h-64 w-full rounded-lg', card: '', 'card-avatar': '', actions: '', table: 'h-4 w-full', 'table-row': 'h-12 w-full', button: 'h-10 w-24 rounded-md', 'button-icon': 'h-10 w-10 rounded-md', badge: 'h-6 w-16 rounded-full', tab: 'h-8 w-24 rounded-md', 'date-picker': 'h-10 w-full', 'list-item': 'h-16 w-full', 'list-item-two-line': 'h-20 w-full', 'list-item-three-line': 'h-24 w-full', }, }, defaultVariants: { variant: 'text', }, }) export type SkeletonLoaderVariants = VariantProps<typeof skeletonLoaderVariants>src/lib/components/ui/skeleton/index.ts0.6 kB
export { default as Skeleton, type SkeletonProps } from './Skeleton.svelte' export { default as SkeletonText, type SkeletonTextProps } from './SkeletonText.svelte' export { default as SkeletonGroup, type SkeletonGroupProps } from './SkeletonGroup.svelte' export { default as SkeletonLoader, type SkeletonLoaderProps } from './SkeletonLoader.svelte' // Re-export variant API from the sibling file (kept separate to avoid the // SkeletonLoader.svelte <-> index.ts circular import that broke dev SSR). export { skeletonLoaderVariants, type SkeletonLoaderVariants } from './skeleton.variants'
Raw manifest:https://uipkge.dev/r/svelte/skeleton.json