展开式面板
一排窄面板中,被悬停或聚焦的那块会变宽并展示内容,其余面板让出空间。
用 CSS grid 在 React 中制作一排展开式面板。容器设置 display: grid,grid-template-columns 由列表生成,例如 3fr 1fr 1fr 1fr,并设置 transition: grid-template-columns 500ms cubic-bezier(0.16, 1, 0.3, 1)(现代浏览器支持对网格轨道做过渡)。在 pointerenter 和 focus 时记录当前序号,并重建列宽列表,让当前面板占 3fr。收起的面板只显示竖排标题(writing-mode: vertical-rl);展开的面板显示标题和一行说明,说明在宽度变化后 150ms 淡入。每个面板都是带 aria-expanded 的 <button>。面板为:策略、设计、开发、增长。在窄屏上把面板改为纵向排列,改为展开高度。在 prefers-reduced-motion 下宽度直接切换。
源代码
'use client'
import { useState } from 'react'
import s from './demo.module.css'
export default function ExpandingPanels({ labels }: { labels: Record<string, string> }) {
const titles = labels.panels.split('|')
const bodies = labels.body.split('|')
const [active, setActive] = useState(0)
return (
<div className={s.row} style={{ gridTemplateColumns: titles.map((_, i) => (i === active ? '3fr' : '1fr')).join(' ') }}>
{titles.map((t, i) => (
<button
key={t}
type="button"
className={s.panel}
aria-expanded={i === active}
onPointerEnter={() => setActive(i)}
onFocus={() => setActive(i)}
onClick={() => setActive(i)}
>
<span className={s.n}>{String(i + 1).padStart(2, '0')}</span>
<span className={s.title}>{t}</span>
<span className={s.body}>{bodies[i]}</span>
</button>
))}
</div>
)
}
.row {
display: grid;
gap: 8px;
width: min(320px, 100%);
height: 180px;
transition: grid-template-columns 500ms var(--ease-out);
}
.panel {
position: relative;
display: grid;
align-content: space-between;
justify-items: start;
min-width: 0;
padding: 12px;
overflow: hidden;
border: 1px solid var(--stage-rule);
border-radius: 14px;
background: var(--stage-surface);
color: var(--stage-ink);
text-align: left;
cursor: pointer;
}
.panel[aria-expanded='true'] { background: var(--stage-accent); border-color: var(--stage-accent); color: var(--stage-accent-ink); }
.panel:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.n { font: 500 var(--text-xs) / 1 var(--font-mono); opacity: 0.7; }
.title {
font: 600 var(--text-base) / 1.1 var(--font-display);
white-space: nowrap;
writing-mode: vertical-rl;
}
.panel[aria-expanded='true'] .title { writing-mode: horizontal-tb; font-size: var(--text-lg); }
.body {
position: absolute;
left: 12px;
right: 12px;
bottom: 12px;
font: 400 var(--text-xs) / 1.4 var(--font-body);
opacity: 0;
}
.panel[aria-expanded='true'] .title { margin-bottom: 40px; }
.panel[aria-expanded='true'] .body { opacity: 1; transition: opacity 250ms var(--ease-out) 150ms; }
@media (prefers-reduced-motion: reduce) {
.row { transition: none; }
}