Expanding Panels

A row of narrow panels where the one you hover or focus widens to show its story while the others make room.

  • CSS
  • Layout
  • Hover
  • Added

AI prompt

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

Demo.tsx
'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>
  )
}
demo.module.css
.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; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.