顶部加载条

页面加载时一条细条在顶部慢慢爬行,越走越慢,加载完成后冲到尽头再淡出。

  • JS
  • 布局
  • 点击
  • 收录于

AI 提示词

用 React 制作页面顶部的加载条。开始时显示一条 3px 的主色进度条,transform: scaleX(0.08),transform-origin 为 left。每隔 300ms 让它前进剩余距离的一部分(progress += (0.9 − progress) × 0.15),使它不断前进、逐渐变慢,但永远不会自己走到终点。transform 加 300ms ease-out 过渡。加载完成时(用 1.8s 的 timeout 模拟),用更快的 200ms 过渡把进度设为 1,然后在 300ms 内淡出并重置。用伪元素的 box-shadow 在前端加一点柔和的光晕。组件卸载时清除所有计时器。在 prefers-reduced-motion 下不做爬行,完成前显示一条静止的进度条。

源代码

Demo.tsx
'use client'

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

type Phase = 'idle' | 'loading' | 'done'

export default function TopLoadingBar({ labels }: { labels: Record<string, string> }) {
  const [phase, setPhase] = useState<Phase>('idle')
  const [p, setP] = useState(0)
  const timers = useRef<number[]>([])

  const clear = () => {
    timers.current.forEach((t) => (clearInterval(t), clearTimeout(t)))
    timers.current = []
  }
  useEffect(() => clear, [])

  const start = () => {
    clear()
    const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
    setPhase('loading')
    setP(reduced ? 0.6 : 0.08)
    if (!reduced) timers.current.push(window.setInterval(() => setP((v) => v + (0.9 - v) * 0.15), 300))
    timers.current.push(
      window.setTimeout(() => {
        clear()
        setPhase('done')
        setP(1)
        timers.current.push(window.setTimeout(() => (setPhase('idle'), setP(0)), 700))
      }, 1800),
    )
  }

  return (
    <div className={s.window}>
      <span className={s.bar} data-phase={phase} style={{ transform: `scaleX(${p})` }} aria-hidden="true" />
      <div className={s.chrome}>
        <span />
        <span />
        <span />
      </div>
      <div className={s.body}>
        <button type="button" className={s.btn} onClick={start} disabled={phase === 'loading'}>
          {labels.load}
        </button>
        <span className={s.status} role="status">
          {phase === 'done' ? labels.loaded : ''}
        </span>
      </div>
    </div>
  )
}
demo.module.css
.window {
  position: relative;
  width: min(300px, 100%);
  overflow: hidden;
  border: 1px solid var(--stage-rule);
  border-radius: 12px;
  background: var(--stage-surface);
}

.bar {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
  width: 100%;
  height: 3px;
  background: var(--stage-accent);
  transform-origin: left;
  opacity: 0;
  transition: transform 300ms var(--ease-out), opacity 300ms;
}

.bar::after {
  content: '';
  position: absolute;
  right: 0;
  top: 0;
  width: 60px;
  height: 100%;
  box-shadow: 0 0 10px 1px var(--stage-accent);
}

.bar[data-phase='loading'] { opacity: 1; }
.bar[data-phase='done'] { opacity: 0; transition: transform 200ms var(--ease-out), opacity 300ms 250ms; }

.chrome {
  display: flex;
  gap: 6px;
  padding: 12px;
  border-bottom: 1px solid var(--stage-rule);
}

.chrome span { width: 8px; height: 8px; border-radius: 50%; background: var(--stage-rule); }

.body {
  display: grid;
  justify-items: center;
  gap: 8px;
  padding: 28px 16px;
}

.btn {
  height: 36px;
  padding: 0 16px;
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--stage-accent);
  color: var(--stage-accent-ink);
  font: 500 var(--text-sm) / 1 var(--font-body);
  cursor: pointer;
}

.btn:disabled { opacity: 0.6; cursor: progress; }
.btn:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

.status {
  min-height: 1em;
  font: 500 var(--text-xs) / 1 var(--font-mono);
  color: var(--stage-muted);
}

@media (prefers-reduced-motion: reduce) {
  .bar, .bar[data-phase='done'] { transition: opacity 150ms; }
}

投稿创意

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

你想投稿的是?

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