轮播词

标题里的一个词向上滑出,下一个词从下方滑入,词槽宽度随之调整。

  • CSS
  • 文字
  • 循环
  • 收录于

让设计感觉 安静

AI 提示词

用 React 制作最后一个词会轮换的标题:“让设计感觉”后面依次显示安静、精准、鲜活、属于你。每 2.2 秒切换一次序号。把所有词叠放在一个 overflow hidden 的 inline-grid 单元格中;当前词位于 translateY(0)、opacity 1,上一个词移动到 translateY(-100%)、opacity 0,其余词在 translateY(100%) 等待。transform 和 opacity 的过渡时间为 500ms,缓动为 cubic-bezier(0.16, 1, 0.3, 1)。让词槽宽度跟随当前词变化:用一个隐藏副本测量宽度,并以 300ms 过渡设置 width。轮换的词使用强调色。用一个视觉隐藏、带 aria-live="polite" 的元素保存包含当前词的完整句子。在 prefers-reduced-motion 下改为 150ms 的淡入淡出切换。

源代码

Demo.tsx
'use client'

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

export default function RotatingWords({ labels }: { labels: Record<string, string> }) {
  const words = labels.words.split('|')
  const [index, setIndex] = useState(0)
  const [width, setWidth] = useState<number>()
  const measure = useRef<HTMLSpanElement>(null)

  useEffect(() => {
    const id = setInterval(() => setIndex((i) => (i + 1) % words.length), 2200)
    return () => clearInterval(id)
  }, [words.length])

  useLayoutEffect(() => {
    if (measure.current) setWidth(measure.current.offsetWidth)
  }, [index, labels.words])

  const prev = (index - 1 + words.length) % words.length

  return (
    <p className={s.line}>
      <span aria-hidden="true">{labels.lead} </span>
      <span className={s.slot} style={{ width }} aria-hidden="true">
        {words.map((w, i) => (
          <span key={w} className={s.word} data-state={i === index ? 'in' : i === prev ? 'out' : 'wait'}>
            {w}
          </span>
        ))}
      </span>
      <span ref={measure} className={s.measure} aria-hidden="true">
        {words[index]}
      </span>
      <span className={s.sr} aria-live="polite">
        {labels.lead} {words[index]}
      </span>
    </p>
  )
}
demo.module.css
.line {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: 0.25em;
  margin: 0;
  font: 600 var(--text-2xl) / 1.2 var(--font-display);
  letter-spacing: -0.02em;
  color: var(--stage-ink);
  white-space: nowrap;
}

.slot {
  display: inline-grid;
  overflow: hidden;
  transition: width 300ms var(--ease-out);
}

.word {
  grid-area: 1 / 1;
  color: var(--stage-accent);
  transition: transform 500ms var(--ease-out), opacity 500ms var(--ease-out);
}

.word[data-state='in'] { transform: translateY(0); opacity: 1; }
.word[data-state='out'] { transform: translateY(-100%); opacity: 0; }
.word[data-state='wait'] { transform: translateY(100%); opacity: 0; transition: none; }

.measure { position: absolute; visibility: hidden; white-space: nowrap; }

.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }

@media (prefers-reduced-motion: reduce) {
  .word, .word[data-state] { transform: none; transition: opacity 150ms linear; }
  .slot { transition: none; }
}

投稿创意

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

你想投稿的是?

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