loading-states

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Loading & Empty States

加载状态与空状态

Maintain user confidence when content isn't immediately available.
当内容无法立即显示时,维持用户的信任感。

Loading State Types

加载状态类型

Choose by Duration

按时长选择

DurationRecommendation
< 100msNo indicator needed
100ms - 1sSubtle indicator (opacity change)
1s - 10sSkeleton screen or spinner
> 10sProgress bar with estimate
时长推荐方案
< 100ms无需加载指示器
100ms - 1s微妙的提示(如透明度变化)
1s - 10sSkeleton 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

空状态类型

  1. First Use: User hasn't added data yet
  2. No Results: Search/filter returned nothing
  3. Error State: Something went wrong
  4. Success Empty: Completed all tasks (inbox zero)
  1. 首次使用:用户尚未添加任何数据
  2. 无结果:搜索/筛选未返回任何内容
  3. 错误状态:出现异常情况
  4. 成功空状态:所有任务已完成(如收件箱清零)

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秒)显示进度
  • 空状态包含有用的操作
  • 错误状态包含重试选项
  • 动画尊重用户的减少动效偏好
  • 屏幕阅读器可宣布加载状态
  • 加载状态不会不必要地阻塞整个页面