Skeleton
surfacesNyGratisMITEn grå pladsholder til indhold, der er på vej, med fast størrelse (bredde, højde, rund), så siden ikke hopper. Pulser stille og står stille ved reduceret bevægelse. SkeletonText tegner tekstlinjer, hvor den sidste er kortere.
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/skeleton.jsonPreview
Live demo i det mørke tema.
Kode
Installér
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/skeleton.jsonMed registret tilføjet (components.json)
npx shadcn@latest add @manniche/skeletonBrug
import { Skeleton, SkeletonText } from '@/components/skeleton'
export default function SkeletonDemo() {
return (
<section aria-label="Skeleton" className="grid w-full gap-6">
<div className="flex items-center gap-3">
<Skeleton circle width={44} />
<div className="grid flex-1 gap-2">
<Skeleton width="45%" height={14} />
<Skeleton width="30%" height={12} />
</div>
</div>
<SkeletonText lines={3} />
<Skeleton height={120} className="rounded-xl" />
<p className="text-[13.5px] text-muted-foreground">
Fixed sizes, the theme's muted colour, and a slow pulse that stands still under reduced motion.
</p>
</section>
)
}Kilde
// A grey stand-in for content that has not arrived yet. It has a fixed size, so the page does not move when the real
// content replaces it: give it the width and height of what it stands for. It pulses slowly; under reduced motion it
// stands still as a flat grey block. The colour is the theme's `muted`, so it follows light and dark by itself.
// Screen readers skip it (`aria-hidden`): the region around it says that something is loading (see loading-region).
import type { ComponentPropsWithoutRef, CSSProperties } from 'react'
import { cn } from '@/lib/utils'
type Size = number | string
const px = (v: Size | undefined) => (typeof v === 'number' ? `${v}px` : v)
export type SkeletonProps = Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'style'> & {
/** Width of the block. A number is pixels; a string is any CSS length ("100%", "12rem"). Default "100%". */
width?: Size
/** Height of the block, the same units. Default 16. */
height?: Size
/** A circle of the given `width` (and the same height), for avatars. */
circle?: boolean
/** Classes for the block. */
className?: string
}
export function Skeleton({ width = '100%', height = 16, circle = false, className, ...rest }: SkeletonProps) {
const style: CSSProperties = { width: px(width), height: px(circle ? width : height) }
return (
<div
aria-hidden="true"
{...rest}
style={style}
className={cn('max-w-full shrink-0 bg-muted motion-safe:animate-pulse', circle ? 'rounded-full' : 'rounded-md', className)}
/>
)
}
export type SkeletonTextProps = Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'style'> & {
/** How many lines. Default 3. */
lines?: number
/** Height of each line in pixels. Default 14. */
lineHeight?: number
/** Space between the lines in pixels. Default 8. */
gap?: number
/** Width of the last line, as a share of the box. Default "60%". */
lastLineWidth?: Size
/** Classes for the box. */
className?: string
}
/** Lines of text, the last one shorter. Its height is lines x lineHeight + gaps, so it can match a paragraph. */
export function SkeletonText({ lines = 3, lineHeight = 14, gap = 8, lastLineWidth = '60%', className, ...rest }: SkeletonTextProps) {
const count = Math.max(1, Math.floor(lines))
return (
<div aria-hidden="true" {...rest} className={cn('flex w-full flex-col', className)} style={{ gap }}>
{Array.from({ length: count }, (_, i) => (
<Skeleton key={i} height={lineHeight} width={i === count - 1 && count > 1 ? lastLineWidth : '100%'} />
))}
</div>
)
}Props
SkeletonProps
| Prop | Type | Standard | Hvad den gør |
|---|---|---|---|
width | Size | '100%' | Width of the block. A number is pixels; a string is any CSS length ("100%", "12rem"). Default "100%". |
height | Size | 16 | Height of the block, the same units. Default 16. |
circle | boolean | false | A circle of the given width (and the same height), for avatars. |
className | string | – | Classes for the block. |
Tager også alle props fra Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'style'>.
A11y
Skærmlæser
Every block is aria-hidden, so a screen reader hears nothing from it. The region around it (loading-region) says that something is loading; used alone, give the container aria-busy and a role="status" line.
Mindre bevægelse
The slow pulse is motion-safe only: under reduced motion the block is a flat grey, with no shimmer and no movement. Sizes are fixed, so the swap to real content moves nothing.