expandable-card

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Expandable Card Pattern

可展开卡片模式

Build smooth expand/collapse animations using CSS grid-rows, avoiding height:auto animation issues.
使用CSS grid-rows构建流畅的展开/折叠动画,避免height:auto的动画问题。

Why grid-rows?

为什么选择grid-rows?

Traditional height animation requires explicit pixel values. The
grid-rows
technique allows smooth animation to/from
auto
height:
  • grid-rows-[0fr]
    +
    overflow-hidden
    = collapsed (0 height)
  • grid-rows-[1fr]
    = expanded (natural height)
传统的高度动画需要明确的像素值。
grid-rows
技术支持平滑地在
auto
高度之间切换动画:
  • grid-rows-[0fr]
    +
    overflow-hidden
    = 折叠状态(高度为0)
  • grid-rows-[1fr]
    = 展开状态(自然高度)

Core Implementation

核心实现

tsx
"use client";

import { useState } from "react";
import { ChevronDown } from "lucide-react";

function ExpandableCard() {
  const [expanded, setExpanded] = useState(true);

  return (
    <div className="rounded-xl border overflow-hidden transition-all duration-300">
      {/* Header - clickable toggle */}
      <div
        className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-zinc-50 transition-colors"
        onClick={() => setExpanded(!expanded)}
      >
        <span className="font-medium">Card Title</span>
        <ChevronDown
          className={`w-5 h-5 transition-transform duration-200 ${
            expanded ? "rotate-180" : ""
          }`}
        />
      </div>

      {/* Content - animated container */}
      <div
        className={`grid transition-all duration-300 ease-in-out ${
          expanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
        }`}
      >
        <div className="overflow-hidden">
          <div className="px-4 py-4 border-t">
            {/* Your content here */}
            <p>Expandable content goes here.</p>
          </div>
        </div>
      </div>
    </div>
  );
}
tsx
"use client";

import { useState } from "react";
import { ChevronDown } from "lucide-react";

function ExpandableCard() {
  const [expanded, setExpanded] = useState(true);

  return (
    <div className="rounded-xl border overflow-hidden transition-all duration-300">
      {/* 头部 - 可点击的切换区域 */}
      <div
        className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-zinc-50 transition-colors"
        onClick={() => setExpanded(!expanded)}
      >
        <span className="font-medium">卡片标题</span>
        <ChevronDown
          className={`w-5 h-5 transition-transform duration-200 ${
            expanded ? "rotate-180" : ""
          }`}
        />
      </div>

      {/* 内容 - 动画容器 */}
      <div
        className={`grid transition-all duration-300 ease-in-out ${
          expanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
        }`}
      >
        <div className="overflow-hidden">
          <div className="px-4 py-4 border-t">
            {/* 在此处添加你的内容 */}
            <p>可展开内容放置于此。</p>
          </div>
        </div>
      </div>
    </div>
  );
}

Key Elements

关键元素

1. State Management

1. 状态管理

tsx
const [expanded, setExpanded] = useState(true); // Start expanded
// or
const [expanded, setExpanded] = useState(false); // Start collapsed
tsx
const [expanded, setExpanded] = useState(true); // 默认展开
// 或者
const [expanded, setExpanded] = useState(false); // 默认折叠

2. Header Click Handler

2. 头部点击事件处理

tsx
<div
  className="cursor-pointer hover:bg-zinc-50 transition-colors"
  onClick={() => setExpanded(!expanded)}
>
tsx
<div
  className="cursor-pointer hover:bg-zinc-50 transition-colors"
  onClick={() => setExpanded(!expanded)}
>

3. ChevronDown Rotation

3. ChevronDown 旋转效果

tsx
<ChevronDown
  className={`transition-transform duration-200 ${
    expanded ? "rotate-180" : ""
  }`}
/>
tsx
<ChevronDown
  className={`transition-transform duration-200 ${
    expanded ? "rotate-180" : ""
  }`}
/>

4. Grid Container Animation

4. Grid容器动画

tsx
<div
  className={`grid transition-all duration-300 ease-in-out ${
    expanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
  }`}
>
  <div className="overflow-hidden">
    {/* Content wrapper - REQUIRED for animation */}
  </div>
</div>
tsx
<div
  className={`grid transition-all duration-300 ease-in-out ${
    expanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
  }`}
>
  <div className="overflow-hidden">
    {/* 内容包裹层 - 动画必需 */}
  </div>
</div>

Timing Recommendations

时长建议

DurationUse Case
duration-150
Small cards, quick feedback
duration-200
Chevron rotation
duration-300
Content expansion (recommended)
duration-500
Large content areas
时长值使用场景
duration-150
小型卡片,快速反馈
duration-200
Chevron图标旋转
duration-300
内容展开(推荐值)
duration-500
大型内容区域

Shadow Transition (Optional)

阴影过渡(可选)

Add shadow that changes with state:
tsx
<div
  className={`rounded-xl border overflow-hidden transition-all duration-300 ${
    expanded ? "shadow-lg" : "shadow-sm hover:shadow-md"
  }`}
>
添加随状态变化的阴影效果:
tsx
<div
  className={`rounded-xl border overflow-hidden transition-all duration-300 ${
    expanded ? "shadow-lg" : "shadow-sm hover:shadow-md"
  }`}
>

Accessibility Considerations

无障碍适配考虑

tsx
<div
  role="button"
  tabIndex={0}
  aria-expanded={expanded}
  onClick={() => setExpanded(!expanded)}
  onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      setExpanded(!expanded);
    }
  }}
>
tsx
<div
  role="button"
  tabIndex={0}
  aria-expanded={expanded}
  onClick={() => setExpanded(!expanded)}
  onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      setExpanded(!expanded);
    }
  }}
>

Common Variations

常见变体

Multiple Cards (Accordion)

多卡片模式(手风琴)

tsx
const [expandedId, setExpandedId] = useState<string | null>("first");

// Toggle logic
onClick={() => setExpandedId(expandedId === id ? null : id)}
tsx
const [expandedId, setExpandedId] = useState<string | null>("first");

// 切换逻辑
onClick={() => setExpandedId(expandedId === id ? null : id)}

Nested Content Protection

嵌套内容防护

Prevent clicks on interactive content from toggling:
tsx
<a
  href="..."
  onClick={(e) => e.stopPropagation()}
>
Important: Always add
stopPropagation
to:
  • Links (
    <a>
    )
  • Buttons that perform actions other than toggling
  • Form inputs
  • Any interactive element that shouldn't trigger expand/collapse
tsx
// Full example with multiple interactive elements
<div className="overflow-hidden">
  <div className="px-4 py-4 border-t">
    <a
      href="https://example.com"
      onClick={(e) => e.stopPropagation()}
      className="text-blue-500 hover:underline"
    >
      External link
    </a>

    <button
      onClick={(e) => {
        e.stopPropagation();
        // Handle button action
      }}
    >
      Action Button
    </button>
  </div>
</div>
防止点击交互内容时触发折叠/展开:
tsx
<a
  href="..."
  onClick={(e) => e.stopPropagation()}
>
重要提示:务必为以下元素添加
stopPropagation
  • 链接 (
    <a>
    )
  • 执行切换以外操作的按钮
  • 表单输入框
  • 任何不应触发展开/折叠的交互元素
tsx
// 包含多个交互元素的完整示例
<div className="overflow-hidden">
  <div className="px-4 py-4 border-t">
    <a
      href="https://example.com"
      onClick={(e) => e.stopPropagation()}
      className="text-blue-500 hover:underline"
    >
      外部链接
    </a>

    <button
      onClick={(e) => {
        e.stopPropagation();
        // 处理按钮操作
      }}
    >
      操作按钮
    </button>
  </div>
</div>

Checklist

检查清单

  • overflow-hidden
    on inner wrapper (required for animation)
  • transition-all
    on grid container
  • ChevronDown has
    transition-transform
  • Header has
    cursor-pointer
    and hover state
  • Timing consistent (300ms recommended)
  • 内部包裹层添加
    overflow-hidden
    (动画必需)
  • Grid容器添加
    transition-all
  • ChevronDown添加
    transition-transform
  • 头部添加
    cursor-pointer
    和悬停状态
  • 时长保持一致(推荐300ms)