滑动标签指示器

一块高亮在标签之间滑动并改变宽度,而不是每个标签各自亮起。

  • CSS
  • 表单与控件
  • 点击
  • 收录于

AI 提示词

用 React 制作一个带单个滑动指示器、符合无障碍规范的标签组件。渲染一个 role="tablist",包含“概览”“用法”“API 参考”三个按钮(role="tab",aria-selected)。在标签后方保留一个绝对定位的指示器元素。当前标签改变时,测量它的 offsetLeft 和 offsetWidth,用 transform: translateX(...) 和基于宽度的 scaleX(或通过 CSS 变量设置宽度)移动指示器,过渡时间 260ms,缓动为 cubic-bezier(0.16, 1, 0.3, 1)。支持用左右方向键切换标签。窗口尺寸变化时重新测量。在 prefers-reduced-motion 下让指示器直接跳到目标位置。

源代码

Demo.tsx
'use client'

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

export default function SlidingTabs({ labels }: { labels: Record<string, string> }) {
  const tabs = labels.tabs.split('|')
  const [active, setActive] = useState(0)
  const list = useRef<HTMLDivElement>(null)
  const indicator = useRef<HTMLSpanElement>(null)

  useLayoutEffect(() => {
    const place = () => {
      const tab = list.current?.children[active + 1] as HTMLElement | undefined
      if (!tab || !indicator.current) return
      indicator.current.style.width = `${tab.offsetWidth}px`
      indicator.current.style.transform = `translateX(${tab.offsetLeft}px)`
    }
    place()
    window.addEventListener('resize', place)
    return () => window.removeEventListener('resize', place)
  }, [active, labels.tabs])

  const onKey = (e: React.KeyboardEvent) => {
    const step = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0
    if (!step) return
    const next = (active + step + tabs.length) % tabs.length
    setActive(next)
    ;(list.current?.children[next + 1] as HTMLElement | undefined)?.focus()
  }

  return (
    <div ref={list} className={s.list} role="tablist" onKeyDown={onKey}>
      <span ref={indicator} className={s.indicator} aria-hidden="true" />
      {tabs.map((t, i) => (
        <button
          key={t}
          type="button"
          role="tab"
          aria-selected={i === active}
          tabIndex={i === active ? 0 : -1}
          className={s.tab}
          onClick={() => setActive(i)}
        >
          {t}
        </button>
      ))}
    </div>
  )
}
demo.module.css
.list {
  position: relative;
  display: inline-flex;
  padding: 4px;
  border: 1px solid var(--stage-rule);
  border-radius: var(--radius-md);
  background: var(--stage-surface);
}

.indicator {
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: 0;
  border-radius: var(--radius-sm);
  background: var(--stage-ink);
  transition: transform 260ms var(--ease-out), width 260ms var(--ease-out);
}

.tab {
  position: relative;
  height: 36px;
  padding: 0 14px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--stage-muted);
  font: 500 var(--text-sm) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
  transition: color 260ms var(--ease-out);
}

.tab[aria-selected='true'] { color: var(--stage-bg); }
.tab:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .indicator, .tab { transition: none; }
}

投稿创意

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

你想投稿的是?

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