diff --git a/src/design-system/primitives/Skeleton/Skeleton.module.css b/src/design-system/primitives/Skeleton/Skeleton.module.css
new file mode 100644
index 0000000..d801a0c
--- /dev/null
+++ b/src/design-system/primitives/Skeleton/Skeleton.module.css
@@ -0,0 +1,48 @@
+@keyframes shimmer {
+ from {
+ background-position: -200% center;
+ }
+ to {
+ background-position: 200% center;
+ }
+}
+
+.skeleton {
+ background-color: var(--bg-inset);
+ display: block;
+}
+
+.shimmer {
+ background-image: linear-gradient(
+ 90deg,
+ var(--bg-inset) 25%,
+ color-mix(in srgb, var(--bg-inset) 60%, transparent) 50%,
+ var(--bg-inset) 75%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s linear infinite;
+}
+
+.text {
+ height: 12px;
+ border-radius: 4px;
+ width: 100%;
+}
+
+.textGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.circular {
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+}
+
+.rectangular {
+ border-radius: var(--radius-sm);
+ width: 100%;
+ height: 80px;
+}
diff --git a/src/design-system/primitives/Skeleton/Skeleton.test.tsx b/src/design-system/primitives/Skeleton/Skeleton.test.tsx
new file mode 100644
index 0000000..bc5177d
--- /dev/null
+++ b/src/design-system/primitives/Skeleton/Skeleton.test.tsx
@@ -0,0 +1,82 @@
+import { describe, it, expect } from 'vitest'
+import { render } from '@testing-library/react'
+import { Skeleton } from './Skeleton'
+
+describe('Skeleton', () => {
+ it('renders rectangular variant by default', () => {
+ const { container } = render()
+ const el = container.firstChild as HTMLElement
+ expect(el).toHaveClass('rectangular')
+ expect(el).toHaveClass('shimmer')
+ })
+
+ it('renders text variant with single bar', () => {
+ const { container } = render()
+ const el = container.firstChild as HTMLElement
+ expect(el).toHaveClass('text')
+ expect(el).toHaveClass('shimmer')
+ })
+
+ it('renders circular variant', () => {
+ const { container } = render()
+ const el = container.firstChild as HTMLElement
+ expect(el).toHaveClass('circular')
+ expect(el).toHaveClass('shimmer')
+ })
+
+ it('renders multiple lines for text variant when lines > 1', () => {
+ const { container } = render()
+ const group = container.firstChild as HTMLElement
+ expect(group).toHaveClass('textGroup')
+ const bars = group.querySelectorAll('.text')
+ expect(bars).toHaveLength(3)
+ })
+
+ it('renders last bar at 70% width when lines > 1', () => {
+ const { container } = render()
+ const group = container.firstChild as HTMLElement
+ const bars = group.querySelectorAll('.text')
+ const lastBar = bars[bars.length - 1] as HTMLElement
+ expect(lastBar.style.width).toBe('70%')
+ })
+
+ it('applies custom width and height', () => {
+ const { container } = render()
+ const el = container.firstChild as HTMLElement
+ expect(el.style.width).toBe('200px')
+ expect(el.style.height).toBe('50px')
+ })
+
+ it('applies custom width and height as strings', () => {
+ const { container } = render()
+ const el = container.firstChild as HTMLElement
+ expect(el.style.width).toBe('80px')
+ expect(el.style.height).toBe('80px')
+ })
+
+ it('applies extra className', () => {
+ const { container } = render()
+ const el = container.firstChild as HTMLElement
+ expect(el).toHaveClass('custom-class')
+ })
+
+ it('circular has default 40x40 dimensions', () => {
+ const { container } = render()
+ const el = container.firstChild as HTMLElement
+ expect(el.style.width).toBe('40px')
+ expect(el.style.height).toBe('40px')
+ })
+
+ it('rectangular has default 80px height and 100% width', () => {
+ const { container } = render()
+ const el = container.firstChild as HTMLElement
+ expect(el.style.width).toBe('100%')
+ expect(el.style.height).toBe('80px')
+ })
+
+ it('is aria-hidden', () => {
+ const { container } = render()
+ const el = container.firstChild as HTMLElement
+ expect(el).toHaveAttribute('aria-hidden', 'true')
+ })
+})
diff --git a/src/design-system/primitives/Skeleton/Skeleton.tsx b/src/design-system/primitives/Skeleton/Skeleton.tsx
new file mode 100644
index 0000000..75e3532
--- /dev/null
+++ b/src/design-system/primitives/Skeleton/Skeleton.tsx
@@ -0,0 +1,71 @@
+import styles from './Skeleton.module.css'
+
+interface SkeletonProps {
+ variant?: 'text' | 'circular' | 'rectangular'
+ width?: string | number
+ height?: string | number
+ lines?: number
+ className?: string
+}
+
+function normalizeSize(value: string | number | undefined): string | undefined {
+ if (value === undefined) return undefined
+ return typeof value === 'number' ? `${value}px` : value
+}
+
+export function Skeleton({
+ variant = 'rectangular',
+ width,
+ height,
+ lines,
+ className,
+}: SkeletonProps) {
+ const w = normalizeSize(width)
+ const h = normalizeSize(height)
+
+ if (variant === 'text') {
+ const lineCount = lines && lines > 1 ? lines : 1
+ if (lineCount === 1) {
+ return (
+
+ )
+ }
+ return (
+
+ {Array.from({ length: lineCount }, (_, i) => (
+
+ ))}
+
+ )
+ }
+
+ if (variant === 'circular') {
+ return (
+
+ )
+ }
+
+ // rectangular (default)
+ return (
+
+ )
+}