翻牌显示

像机场的出发信息牌,每一块翻牌依次翻到下一个词,带一点错开,让变化像波浪一样掠过。

  • JS
  • 文字
  • 循环
  • 收录于
北京

AI 提示词

用 React 和 CSS 制作循环切换词语的翻牌显示板。把每个词用空格补到最长词的长度,每个字符渲染为一块翻牌(等宽字体、深色底、用伪元素在中间画一条 1px 的横向缝)。每 2.4s 切换到下一个词。给每块翻牌设置 key 为 `${index}-${char}`,这样 React 只会重新挂载字符有变化的翻牌;挂载时播放 CSS keyframe:从 rotateX(-90deg)、opacity 0.4 到 rotateX(0),时长 360ms,缓动 cubic-bezier(0.16, 1, 0.3, 1),transform-origin 居中,父元素设置 perspective 400px。每块翻牌通过内联 animation-delay 延迟 index × 60ms。用一个视觉隐藏的 span 配合 aria-live="polite" 播报当前词,翻牌本身设为 aria-hidden。离开屏幕时暂停计时器,在 prefers-reduced-motion 下只切换文字不做翻转。词语为北京、上海、成都、广州。

源代码

Demo.tsx
'use client'

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

const INTERVAL = 2400

export default function SplitFlap({ labels }: { labels: Record<string, string> }) {
  const words = labels.words.split('|')
  const width = Math.max(...words.map((w) => Array.from(w).length))
  const [i, setI] = useState(0)
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const el = ref.current
    if (!el) return
    let timer = 0
    const io = new IntersectionObserver(([e]) => {
      clearInterval(timer)
      if (e.isIntersecting) timer = window.setInterval(() => setI((n) => (n + 1) % words.length), INTERVAL)
    })
    io.observe(el)
    return () => {
      io.disconnect()
      clearInterval(timer)
    }
  }, [words.length])

  const chars = Array.from(words[i].padEnd(width, ' '))

  return (
    <div ref={ref} className={s.board}>
      <span className={s.sr} aria-live="polite">
        {words[i]}
      </span>
      {chars.map((c, n) => (
        <span key={`${n}-${c}`} className={s.tile} style={{ animationDelay: `${n * 60}ms` }} aria-hidden="true">
          {c}
        </span>
      ))}
    </div>
  )
}
demo.module.css
.board {
  display: flex;
  gap: 4px;
  perspective: 400px;
}

.tile {
  position: relative;
  display: grid;
  place-items: center;
  width: 36px;
  height: 52px;
  border-radius: 6px;
  background: var(--stage-surface);
  box-shadow: inset 0 0 0 1px var(--stage-rule);
  color: var(--stage-ink);
  font: 600 var(--text-2xl) / 1 var(--font-mono);
  white-space: pre;
  animation: flip 360ms var(--ease-out) both;
}

.tile::after {
  content: '';
  position: absolute;
  inset: 50% 0 auto;
  height: 1px;
  background: var(--stage-bg);
}

@keyframes flip {
  from { transform: rotateX(-90deg); opacity: 0.4; }
  to { transform: rotateX(0); opacity: 1; }
}

.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (max-width: 360px) {
  .tile { width: 30px; height: 44px; font-size: var(--text-xl); }
}

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

投稿创意

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

你想投稿的是?

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