Expanding Panels
A row of narrow panels where the one you hover or focus widens to show its story while the others make room.
Build an expanding panel row in React with CSS grid. The container has display: grid and grid-template-columns set from a list, for example 3fr 1fr 1fr 1fr, with transition: grid-template-columns 500ms cubic-bezier(0.16, 1, 0.3, 1) (grid tracks are animatable in modern browsers). Track the active index on pointerenter and focus, and rebuild the column list so the active panel gets 3fr. Collapsed panels show only a vertical title (writing-mode: vertical-rl); the active panel shows its title and a one-line description that fades in 150ms after the width change. Each panel is a <button> with aria-expanded. Panels: Strategy, Design, Build, Grow. On narrow screens, stack the panels vertically and expand height instead. Under prefers-reduced-motion, change widths instantly.
Source
'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; }
}