-
Notifications
You must be signed in to change notification settings - Fork 283
Expand file tree
/
Copy pathSkeleton.tsx
More file actions
118 lines (111 loc) · 2.92 KB
/
Copy pathSkeleton.tsx
File metadata and controls
118 lines (111 loc) · 2.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
'use client';
import React from 'react';
/**
* Props for a single Skeleton block.
*/
export interface SkeletonProps {
/**
* Tailwind width class or arbitrary value.
* @example "w-full" | "w-48" | "w-[200px]"
* @default "w-full"
*/
width?: string;
/**
* Tailwind height class or arbitrary value.
* @example "h-4" | "h-10" | "h-[48px]"
* @default "h-4"
*/
height?: string;
/**
* Tailwind border-radius class.
* @default "rounded-md"
*/
rounded?: string;
/** Additional Tailwind classes forwarded to the block. */
className?: string;
}
/**
* Skeleton — a single themed shimmer block.
*
* Accessibility:
* - Carries `aria-hidden="true"` — visual decoration only. Use
* `<SkeletonContainer>` (or a host element with `role="status"`) to
* announce the loading state to screen readers.
*
* Design tokens:
* - Background: `--muted` (adapts to light/dark via the project token set).
* - Shimmer: `animate-pulse` (suppressed under `prefers-reduced-motion`
* by the project-wide globals.css rule; `motion-reduce:animate-none` is
* included as belt-and-suspenders).
*
* @example
* ```tsx
* <Skeleton width="w-48" height="h-5" />
* ```
*/
export const Skeleton: React.FC<SkeletonProps> = ({
width = 'w-full',
height = 'h-4',
rounded = 'rounded-md',
className = '',
}) => (
<div
aria-hidden="true"
className={[
width,
height,
rounded,
'bg-[var(--muted,theme(colors.slate.200))]',
'animate-pulse motion-reduce:animate-none',
className,
]
.filter(Boolean)
.join(' ')}
/>
);
// ---------------------------------------------------------------------------
// SkeletonContainer
// ---------------------------------------------------------------------------
export interface SkeletonContainerProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Accessible label for the loading region (e.g. "Loading payment stream form").
*/
label: string;
children: React.ReactNode;
}
/**
* SkeletonContainer — wraps a group of Skeleton blocks in a `role="status"`
* region so screen readers announce the loading state as soon as it mounts.
*
* ```tsx
* <SkeletonContainer label="Loading contract summary">
* <Skeleton width="w-48" height="h-6" />
* <Skeleton width="w-full" height="h-4" />
* </SkeletonContainer>
* ```
*/
export const SkeletonContainer: React.FC<SkeletonContainerProps> = ({
label,
children,
className = '',
// Destructure out ARIA attributes we control to prevent accidental overrides
role: _role,
'aria-label': _ariaLabel,
'aria-live': _ariaLive,
'aria-busy': _ariaBusy,
...rest
}) => (
<div
{...rest}
role="status"
aria-label={label}
aria-live="polite"
aria-busy="true"
className={className}
>
{children}
{/* Visually hidden textual label so AT reads the container purpose */}
<span className="sr-only">{label}</span>
</div>
);
export default Skeleton;