Skeleton
Interactive UI Explorer
아래에서 각 컴포넌트의 다양한 Variants와 States를 문서화된 형태로 테스트해볼 수 있습니다.
Live Preview
Skeleton
데이터 로딩 중 표시할 placeholder. text · circle · rect 변형을 조합해 카드/리스트 모양을 즉석으로 만들 수 있습니다.
Variants
기본 도형 3종.
Text
Live Preview
Circle
Live Preview
Rect
Live Preview
Multi-line text
lines props로 여러 줄을 한 번에. 마지막 줄은 자동으로 짧게.
Source Code
Live Preview
Card composition
기본 부품을 조합해 콘텐츠 카드 모양을 만든 예시. 실제 Card 골격이 들어갈 자리에 그대로 배치합니다.
Source Code
Live Preview
Implementation
제작 코드
이 컴포넌트가 실제로 어떻게 구현되어 있는지 — 본체 .tsx 파일을 그대로 보여줍니다. variant 매핑·접근성 처리·forwardRef 패턴 등 디테일을 그대로 확인할 수 있어요.
Skeletontypescript
'use client';
/**
* Skeleton — 데이터 로딩 placeholder.
*
* - variant: text(여러 줄 행) · circle(아바타) · rect(박스)
* - shimmer 애니메이션은 Tailwind `animate-pulse` 기반 (저비용)
* - lines props로 text variant에서 여러 줄 동시 렌더 가능
*/
import { forwardRef, type CSSProperties, type HTMLAttributes } from 'react';
import { cn } from '@/lib/cn';
export type SkeletonVariant = 'text' | 'circle' | 'rect';
export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
variant?: SkeletonVariant;
/** width — 숫자(px) 또는 CSS 값 ('100%', '12rem'). default: 100% */
width?: number | string;
/** height — 숫자(px) 또는 CSS 값. default: variant별 적정값 */
height?: number | string;
/** rounded radius (text/rect만). default: variant별 */
radius?: number | string;
/** text variant 전용 — 여러 줄 동시 렌더 */
lines?: number;
/** 마지막 줄을 짧게 (text + lines>1일 때 default true) */
lastLineShorter?: boolean;
}
function toCss(v: number | string | undefined): string | undefined {
if (v == null) return undefined;
return typeof v === 'number' ? `${v}px` : v;
}
const variantDefaults: Record<SkeletonVariant, { width: string; height: string; radius: string }> =
{
text: { width: '100%', height: '0.875rem', radius: '0.5rem' },
circle: { width: '2.5rem', height: '2.5rem', radius: '9999px' },
rect: { width: '100%', height: '6rem', radius: '0.75rem' },
};
export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
(
{
variant = 'text',
width,
height,
radius,
lines,
lastLineShorter = true,
className,
style,
...rest
},
ref,
) => {
const defaults = variantDefaults[variant];
const w = toCss(width) ?? defaults.width;
const h = toCss(height) ?? defaults.height;
const r = toCss(radius) ?? defaults.radius;
const baseClass = cn(
'block animate-pulse bg-gradient-to-r from-slate-100 via-slate-200/70 to-slate-100',
className,
);
const baseStyle: CSSProperties = {
width: w,
height: h,
borderRadius: r,
...style,
};
if (variant === 'text' && lines && lines > 1) {
return (
<div ref={ref} className="flex flex-col gap-2" {...rest}>
{Array.from({ length: lines }).map((_, i) => {
const isLast = i === lines - 1;
return (
<span
key={i}
aria-hidden
className={baseClass}
style={{
...baseStyle,
width: isLast && lastLineShorter ? '70%' : w,
}}
/>
);
})}
</div>
);
}
return <div ref={ref} aria-hidden className={baseClass} style={baseStyle} {...rest} />;
},
);
Skeleton.displayName = 'Skeleton';