骨架屏微光

灰色占位形状先撑住布局,一道柔光从上面扫过;数据到达后,真实内容以淡入方式替换。

  • CSS
  • 卡片
  • 循环
  • 收录于

AI 提示词

用 React 制作一个带 CSS 骨架屏加载状态的个人资料行。加载时,渲染一个 40px 圆形和两条浅灰色长条(宽度分别为 60% 和 40%),每个形状都有一个 ::after 光带:从透明到 60% 不透明度白色再回到透明的 linear-gradient,用 1.4 秒 linear infinite 的 keyframe 从 -100% 平移到 100%。只对 transform 做动画。加载时在容器上设置 aria-busy="true"。数据到达后,在 200ms 内淡入切换到真实内容(头像文字、姓名“林美”、职位“动效设计师”)。在 prefers-reduced-motion 下停止光带扫动,保留静态灰色形状。添加一个按钮在两种状态之间切换。

源代码

Demo.tsx
'use client'

import { useState } from 'react'
import s from './demo.module.css'

export default function SkeletonShimmer({ labels }: { labels: Record<string, string> }) {
  const [loading, setLoading] = useState(true)

  return (
    <div className={s.wrap}>
      <div className={s.row} aria-busy={loading} aria-live="polite">
        {loading ? (
          <>
            <span className={`${s.bone} ${s.avatar}`} />
            <span className={s.lines}>
              <span className={s.bone} style={{ width: '60%' }} />
              <span className={s.bone} style={{ width: '40%' }} />
            </span>
          </>
        ) : (
          <div className={s.loaded}>
            <span className={s.avatar + ' ' + s.initials}>{Array.from(labels.name)[0]}</span>
            <span className={s.lines}>
              <strong className={s.name}>{labels.name}</strong>
              <span className={s.role}>{labels.role}</span>
            </span>
          </div>
        )}
      </div>
      <button type="button" className={s.toggle} onClick={() => setLoading(!loading)}>
        {loading ? labels.load : labels.reset}
      </button>
    </div>
  )
}
demo.module.css
.wrap {
  display: grid;
  justify-items: center;
  gap: 16px;
  width: min(280px, 100%);
}

.row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 64px;
  padding: 12px;
  border: 1px solid var(--stage-rule);
  border-radius: var(--radius-md);
  background: var(--stage-surface);
}

.loaded {
  display: contents;
  animation: fade 200ms var(--ease-out);
}

.lines {
  display: grid;
  flex: 1;
  gap: 8px;
  min-width: 0;
}

.bone {
  position: relative;
  display: block;
  height: 10px;
  overflow: hidden;
  border-radius: 4px;
  background: var(--stage-rule);
}

.bone::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--stage-sheen), transparent);
  transform: translateX(-100%);
  animation: sweep 1.4s linear infinite;
}

.avatar {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
}

.initials {
  display: grid;
  place-items: center;
  background: var(--stage-accent);
  color: var(--stage-accent-ink);
  font: 600 var(--text-sm) / 1 var(--font-display);
}

.name { font: 600 var(--text-sm) / 1.2 var(--font-display); color: var(--stage-ink); }
.role { font-size: var(--text-xs); color: var(--stage-muted); }

.toggle {
  height: 32px;
  padding: 0 12px;
  border: 1px solid var(--stage-rule);
  border-radius: var(--radius-sm);
  background: var(--stage-surface);
  color: var(--stage-ink);
  font: 500 var(--text-sm) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.toggle:hover { border-color: var(--stage-accent); }
.toggle:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

@keyframes sweep { to { transform: translateX(100%); } }
@keyframes fade { from { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .bone::after { animation: none; display: none; }
}

投稿创意

看到想收录的动效或主题?简单描述一下,会打开一个预先填好的 GitHub Issue,确认后即可提交。

你想投稿的是?

需要 GitHub 账号,投稿内容会公开。