loading-states
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLoading & Empty States
加载状态与空状态
Maintain user confidence when content isn't immediately available.
当内容无法立即显示时,维持用户的信任感。
Loading State Types
加载状态类型
Choose by Duration
按时长选择
| Duration | Recommendation |
|---|---|
| < 100ms | No indicator needed |
| 100ms - 1s | Subtle indicator (opacity change) |
| 1s - 10s | Skeleton screen or spinner |
| > 10s | Progress bar with estimate |
| 时长 | 推荐方案 |
|---|---|
| < 100ms | 无需加载指示器 |
| 100ms - 1s | 微妙的提示(如透明度变化) |
| 1s - 10s | Skeleton Screen 或 Spinner |
| > 10s | 带预估进度的 Progress Bar |
Skeleton Screens
Skeleton Screen 骨架屏
When to Use
适用场景
- Page or section content loading
- Lists, cards, tables
- Better than spinners for known layouts
- 页面或区块内容加载时
- 列表、卡片、表格组件
- 对于已知布局,比Spinner效果更好
Basic Skeleton
基础Skeleton实现
css
.skeleton {
background: #e5e7eb;
border-radius: 4px;
}
/* Animated shimmer */
.skeleton-animated {
background: linear-gradient(
90deg,
#f3f4f6 25%,
#e5e7eb 50%,
#f3f4f6 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}css
.skeleton {
background: #e5e7eb;
border-radius: 4px;
}
/* Animated shimmer */
.skeleton-animated {
background: linear-gradient(
90deg,
#f3f4f6 25%,
#e5e7eb 50%,
#f3f4f6 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}Skeleton Components
Skeleton 组件示例
html
<!-- Text skeleton -->
<div class="skeleton skeleton-text" style="width: 80%"></div>
<div class="skeleton skeleton-text" style="width: 60%"></div>
<!-- Avatar skeleton -->
<div class="skeleton skeleton-avatar"></div>
<!-- Image skeleton -->
<div class="skeleton skeleton-image"></div>css
.skeleton-text {
height: 16px;
margin-bottom: 8px;
}
.skeleton-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.skeleton-image {
width: 100%;
aspect-ratio: 16/9;
}html
<!-- Text skeleton -->
<div class="skeleton skeleton-text" style="width: 80%"></div>
<div class="skeleton skeleton-text" style="width: 60%"></div>
<!-- Avatar skeleton -->
<div class="skeleton skeleton-avatar"></div>
<!-- Image skeleton -->
<div class="skeleton skeleton-image"></div>css
.skeleton-text {
height: 16px;
margin-bottom: 8px;
}
.skeleton-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.skeleton-image {
width: 100%;
aspect-ratio: 16/9;
}Card Skeleton Example
卡片Skeleton示例
html
<article class="card card-skeleton">
<div class="skeleton skeleton-image"></div>
<div class="card-content">
<div class="skeleton skeleton-text" style="width: 70%"></div>
<div class="skeleton skeleton-text" style="width: 90%"></div>
<div class="skeleton skeleton-text" style="width: 50%"></div>
</div>
</article>html
<article class="card card-skeleton">
<div class="skeleton skeleton-image"></div>
<div class="card-content">
<div class="skeleton skeleton-text" style="width: 70%"></div>
<div class="skeleton skeleton-text" style="width: 90%"></div>
<div class="skeleton skeleton-text" style="width: 50%"></div>
</div>
</article>What NOT to Skeleton
不适合使用Skeleton的场景
- Modals (should be instant or loading indicator inside)
- Toasts/notifications
- Dropdown menus
- The skeleton itself shouldn't have a skeleton
- 模态框(Modal):应立即显示或在内部放置加载指示器
- 提示框(Toast)/通知
- 下拉菜单
- 不能给Skeleton本身再添加Skeleton
Spinners
Spinner 加载指示器
When to Use
适用场景
- Unknown content structure
- Short operations (1-3 seconds)
- Small areas (buttons, inputs)
- 内容结构未知时
- 短时间操作(1-3秒)
- 小区域(按钮、输入框)
Simple Spinner
基础Spinner实现
css
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}css
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}Button Loading State
按钮加载状态
css
.button-loading {
position: relative;
color: transparent; /* Hide text */
pointer-events: none;
}
.button-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}css
.button-loading {
position: relative;
color: transparent; /* Hide text */
pointer-events: none;
}
.button-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}Inline Loading Text
行内加载文本
html
<span class="loading-text">
Loading
<span class="loading-dots">
<span>.</span><span>.</span><span>.</span>
</span>
</span>html
<span class="loading-text">
Loading
<span class="loading-dots">
<span>.</span><span>.</span><span>.</span>
</span>
</span>Progress Bars
Progress Bar 进度条
When to Use
适用场景
- Operations > 10 seconds
- File uploads/downloads
- Multi-step processes
- 操作时长>10秒时
- 文件上传/下载
- 多步骤流程
Basic Progress Bar
基础Progress Bar实现
html
<div class="progress">
<div
class="progress-bar"
role="progressbar"
style="width: 65%"
aria-valuenow="65"
aria-valuemin="0"
aria-valuemax="100"
>
65%
</div>
</div>css
.progress {
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: var(--primary);
transition: width 0.3s ease-out;
}html
<div class="progress">
<div
class="progress-bar"
role="progressbar"
style="width: 65%"
aria-valuenow="65"
aria-valuemin="0"
aria-valuemax="100"
>
65%
</div>
</div>css
.progress {
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: var(--primary);
transition: width 0.3s ease-out;
}Indeterminate Progress
不确定进度的Progress Bar
css
.progress-indeterminate .progress-bar {
width: 30%;
animation: indeterminate 1.5s infinite ease-in-out;
}
@keyframes indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}css
.progress-indeterminate .progress-bar {
width: 30%;
animation: indeterminate 1.5s infinite ease-in-out;
}
@keyframes indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}Empty States
Empty State 空状态
Types of Empty States
空状态类型
- First Use: User hasn't added data yet
- No Results: Search/filter returned nothing
- Error State: Something went wrong
- Success Empty: Completed all tasks (inbox zero)
- 首次使用:用户尚未添加任何数据
- 无结果:搜索/筛选未返回任何内容
- 错误状态:出现异常情况
- 成功空状态:所有任务已完成(如收件箱清零)
First Use Empty State
首次使用空状态示例
html
<div class="empty-state">
<img src="illustration.svg" alt="" class="empty-illustration">
<h3 class="empty-title">No projects yet</h3>
<p class="empty-description">
Create your first project to get started
</p>
<button class="button-primary">
Create Project
</button>
</div>html
<div class="empty-state">
<img src="illustration.svg" alt="" class="empty-illustration">
<h3 class="empty-title">No projects yet</h3>
<p class="empty-description">
Create your first project to get started
</p>
<button class="button-primary">
Create Project
</button>
</div>No Results Empty State
无结果空状态示例
html
<div class="empty-state">
<span class="empty-icon">🔍</span>
<h3 class="empty-title">No results found</h3>
<p class="empty-description">
Try adjusting your search or filters
</p>
<button class="button-secondary">
Clear Filters
</button>
</div>html
<div class="empty-state">
<span class="empty-icon">🔍</span>
<h3 class="empty-title">No results found</h3>
<p class="empty-description">
Try adjusting your search or filters
</p>
<button class="button-secondary">
Clear Filters
</button>
</div>Empty State Styles
空状态样式
css
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
text-align: center;
}
.empty-illustration {
width: 200px;
max-width: 100%;
margin-bottom: 24px;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-primary);
}
.empty-description {
font-size: 14px;
color: var(--text-secondary);
max-width: 300px;
margin-bottom: 24px;
}css
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
text-align: center;
}
.empty-illustration {
width: 200px;
max-width: 100%;
margin-bottom: 24px;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-primary);
}
.empty-description {
font-size: 14px;
color: var(--text-secondary);
max-width: 300px;
margin-bottom: 24px;
}Error States
错误状态示例
html
<div class="empty-state error-state">
<span class="empty-icon">⚠️</span>
<h3 class="empty-title">Something went wrong</h3>
<p class="empty-description">
We couldn't load your data. Please try again.
</p>
<button class="button-primary">
Retry
</button>
</div>html
<div class="empty-state error-state">
<span class="empty-icon">⚠️</span>
<h3 class="empty-title">Something went wrong</h3>
<p class="empty-description">
We couldn't load your data. Please try again.
</p>
<button class="button-primary">
Retry
</button>
</div>Best Practices
最佳实践
Do
建议
- Match skeleton layout to actual content
- Show loading state immediately (don't wait)
- Use animations to indicate activity
- Provide progress info when possible
- Include helpful actions in empty states
- Keep messaging friendly and helpful
- Skeleton布局与实际内容保持一致
- 立即显示加载状态(不要等待)
- 使用动画提示用户系统正在运行
- 尽可能提供进度信息
- 在空状态中添加有用的操作按钮
- 保持提示信息友好且有帮助
Don't
避免
- Show spinners for everything
- Use loading states for instant operations
- Leave users without feedback
- Make empty states feel like dead ends
- Animate aggressively (respect motion preferences)
- 所有场景都使用Spinner
- 对即时操作显示加载状态
- 不给用户任何反馈
- 让空状态看起来像死胡同
- 过度动画(尊重用户的动效偏好设置)
Accessibility
无障碍访问(Accessibility)
css
/* Announce loading to screen readers */
.loading-region[aria-busy="true"]::before {
content: "Loading...";
position: absolute;
clip: rect(0, 0, 0, 0);
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
.skeleton-animated {
animation: none;
}
.spinner {
animation-duration: 1.5s;
}
}css
/* 向屏幕阅读器宣布加载状态 */
.loading-region[aria-busy="true"]::before {
content: "Loading...";
position: absolute;
clip: rect(0, 0, 0, 0);
}
/* 尊重减少动效的设置 */
@media (prefers-reduced-motion: reduce) {
.skeleton-animated {
animation: none;
}
.spinner {
animation-duration: 1.5s;
}
}Checklist
检查清单
- Loading appears within 100ms of action
- Skeleton matches content structure
- Progress shown for long operations (>10s)
- Empty states have helpful actions
- Error states include retry option
- Animations respect prefers-reduced-motion
- Screen readers announce loading state
- Loading doesn't block entire page unnecessarily
- 操作后100ms内显示加载状态
- Skeleton布局与内容结构匹配
- 长操作(>10秒)显示进度
- 空状态包含有用的操作
- 错误状态包含重试选项
- 动画尊重用户的减少动效偏好
- 屏幕阅读器可宣布加载状态
- 加载状态不会不必要地阻塞整个页面