Material UI Card 组件族实战:从基础卡片、媒体展示到整卡交互与 Active 状态样式
本篇技术指南基于 Material UI 官方 Card 文档(cards.md),系统讲解 Card 及其配套组件 CardContent、CardHeader、CardMedia、CardActions、CardActionArea 的完整用法。读完本文,你将能够:构建基础/描边卡片、用 Collapse 实现可展开的复杂交互卡片、正确选择 CardMedia 的两种媒体渲染模式、用 CardActionArea 让整张卡片可点击,并用 data-active 属性定制卡片的选中态样式——同时结合 packages/mui-material/src/Card 等源码目录,理解每个组件的渲染机制、默认样式与可定制类名。
Card 组件族总览
在 Material Design 规范中,卡片(Card)是承载"单一主题的内容与操作"的表面。Material UI 用一个主容器加若干配套组件来覆盖各种卡片场景:
- Card:表面层容器,负责把相关组件分组,基于
Paper实现; - CardContent:卡片正文内容的包装器;
- CardHeader:可选的头部包装器,支持头像(avatar)、操作(action)、标题(title)、副标题(subheader)等插槽;
- CardMedia:可选的媒体容器,用于展示图片、视频等;
- CardActions:可选的按钮组包装器,通常位于卡片底部;
- CardActionArea:可选的交互区包装器,让用户与卡片的指定区域(通常是整卡)进行交互。
在仓库中,这六个组件分别位于独立源码目录:
- packages/mui-material/src/Card
- packages/mui-material/src/CardActionArea
- packages/mui-material/src/CardActions
- packages/mui-material/src/CardContent
- packages/mui-material/src/CardHeader
- packages/mui-material/src/CardMedia
官方文档也提醒:虽然卡片可以容纳多个操作、UI 控件和溢出菜单,但应克制使用——卡片的设计初衷是"通向更复杂、更详细信息的入口点"。
基础卡片:Card + CardContent
最小可用的卡片只需 Card 与 CardContent:
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
官方示例 BasicCard.tsx 展示了完整的"每日单词"卡片结构:
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
const bull = (
<Box
component="span"
sx={{ display: 'inline-block', mx: '2px', transform: 'scale(0.8)' }}
>
•
</Box>
);
export default function BasicCard() {
return (
<Card sx={{ minWidth: 275 }}>
<CardContent>
<Typography gutterBottom sx={{ color: 'text.secondary', fontSize: 14 }}>
Word of the Day
</Typography>
<Typography variant="h5" component="div">
be{bull}nev{bull}o{bull}lent
</Typography>
<Typography sx={{ color: 'text.secondary', mb: 1.5 }}>adjective</Typography>
<Typography variant="body2">
well meaning and kindly.
<br />
{'"a benevolent smile"'}
</Typography>
</CardContent>
<CardActions>
<Button size="small">Learn More</Button>
</CardActions>
</Card>
);
}
结合源码可以确认几个默认行为:
- Card 是 Paper 的特化。Card.js 中根节点由
styled(Paper)派生,仅追加一条overflow: 'hidden'样式(这也是后续 CardMedia 图片被裁切、焦点环需要改为内缩的原因)。因此 Card 继承 Paper 的variant、elevation、square、component等全部属性。 raised属性等价于 elevation 8。从 Card.js 可见,raised={true}时向 Paper 传入elevation={8};其 PropTypes 还内置了一条校验:raised与variant="outlined"同时使用时会告警"组合无效"。- CardContent 的默认内边距。CardContent.js 中根节点固定
padding: 16,且当它是最后一个子元素时paddingBottom增大为24,保证卡片底部留白更舒适。
描边卡片:variant="outlined"
设置 variant="outlined" 即可渲染描边卡片。该属性来自继承自 Paper 的 variant(可选值 elevated(默认)/ outlined / filled)。官方示例 OutlinedCard.tsx 将卡片正文抽成可复用片段:
const card = (
<React.Fragment>
<CardContent>
<Typography gutterBottom sx={{ color: 'text.secondary', fontSize: 14 }}>
Word of the Day
</Typography>
{/* ... 同 BasicCard 的正文 ... */}
</CardContent>
<CardActions>
<Button size="small">Learn More</Button>
</CardActions>
</React.Fragment>
);
export default function OutlinedCard() {
return (
<Box sx={{ minWidth: 275 }}>
<Card variant="outlined">{card}</Card>
</Box>
);
}
由于 Card 只是 Paper 的薄封装(见 Card.d.ts 中 CardOwnProps extends DistributiveOmit<PaperOwnProps, 'classes'>),所有 Paper 支持的外观定制手段——elevation 调整、sx 覆写、主题中的 components: { MuiCard: { defaultProps } }——对 Card 同样生效。
复杂交互:可展开的食谱卡片
官方示例 RecipeReviewCard.tsx 展示了桌面端卡片的典型进阶形态:点击右下方的展开箭头(chevron),卡片内容区平滑展开显示完整食谱。其结构为 CardHeader(头像 + 标题 + 更多操作)+ CardMedia + CardContent + CardActions + Collapse。核心代码如下:
import { styled } from '@mui/material/styles';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardMedia from '@mui/material/CardMedia';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Collapse from '@mui/material/Collapse';
import Avatar from '@mui/material/Avatar';
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { red } from '@mui/material/colors';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import MoreVertIcon from '@mui/icons-material/MoreVert';
// 通过 styled 让展开箭头随状态旋转 180 度
interface ExpandMoreProps extends IconButtonProps {
expand: boolean;
}
const ExpandMore = styled((props: ExpandMoreProps) => {
const { expand, ...other } = props;
return <IconButton {...other} />;
})(({ theme }) => ({
marginLeft: 'auto',
transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest,
}),
variants: [
{
props: ({ expand }) => !expand,
style: { transform: 'rotate(0deg)' },
},
{
props: ({ expand }) => !!expand,
style: { transform: 'rotate(180deg)' },
},
],
}));
export default function RecipeReviewCard() {
const [expanded, setExpanded] = React.useState(false);
const handleExpandClick = () => {
setExpanded(!expanded);
};
return (
<Card sx={{ maxWidth: 345 }}>
<CardHeader
avatar={
<Avatar sx={{ bgcolor: red[500] }} aria-label="recipe">
R
</Avatar>
}
action={
<IconButton aria-label="settings">
<MoreVertIcon />
</IconButton>
}
title="Shrimp and Chorizo Paella"
subheader="September 14, 2016"
/>
<CardMedia
component="img"
height="194"
image="/static/images/cards/paella.jpg"
alt="Paella dish"
/>
<CardContent>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
This impressive paella is a perfect party dish ...
</Typography>
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add to favorites">
<FavoriteIcon />
</IconButton>
<IconButton aria-label="share">
<ShareIcon />
</IconButton>
<ExpandMore
expand={expanded}
onClick={handleExpandClick}
aria-expanded={expanded}
aria-label="show more"
>
<ExpandMoreIcon />
</ExpandMore>
</CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent>
<Typography sx={{ marginBottom: 2 }}>Method:</Typography>
<Typography sx={{ marginBottom: 2 }}>
Heat 1/2 cup of the broth in a pot until simmering ...
</Typography>
{/* 完整食谱步骤见 demo 源文件 */}
</CardContent>
</Collapse>
</Card>
);
}
这里有几个可复用的交互技巧:
ExpandMore用styled(IconButton)做条件旋转:箭头是否展开通过expand布尔 prop 传入,用variants声明rotate(0deg)/rotate(180deg)两态,过渡时长取自theme.transitions.duration.shortest;Collapse in={expanded} timeout="auto" unmountOnExit:展开动画时长由组件自动计算,动画结束后从 DOM 卸载,避免隐藏内容仍占用布局与可访问性树;CardActions disableSpacing:disableSpacing关闭卡片底部按钮组默认的 8px 内边距与控件间距,让图标按钮更紧凑。这一点可以从 CardActions.js 得到印证:根节点默认display: flex; align-items: center; padding: 8,且只有当disableSpacing为false时才应用spacing类(给后续兄弟元素加margin-left: 8);CardHeader的插槽化结构:从 cardHeaderClasses.ts 可见,CardHeader 暴露root、avatar、action、content、title、subheader六个样式槽,分别对应示例中的avatar、action、title、subheader属性,需要精细定制头部时可按这些类名覆写。
媒体展示:CardMedia 的两种渲染模式
MediaCard.tsx 演示了用图片强化卡片内容的标准用法:
export default function MediaCard() {
return (
<Card sx={{ maxWidth: 345 }}>
<CardMedia
sx={{ height: 140 }}
image="/static/images/cards/contemplative-reptile.jpg"
title="green iguana"
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Lizard
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Lizards are a widespread group of squamate reptiles, with over 6,000
species, ranging across all continents except Antarctica
</Typography>
</CardContent>
<CardActions>
<Button size="small">Share</Button>
<Button size="small">Learn More</Button>
</CardActions>
</Card>
);
}
默认模式:<div> + 背景图。 从 CardMedia.js 可以看到,CardMedia 根节点默认是 div,样式为 display: block; background-size: cover; background-repeat: no-repeat; background-position: center。当传入 image 且 component 仍是 div 时,源码会把 image 拼进内联 backgroundImage: url(...),同时给根节点加上 role="img" 以提升可访问性(CardMedia.js)。
局限与替代:component 属性。 背景图方案在某些场景并不合适——比如需要显示视频、或需要响应式 <img>。此时应使用 component 属性让 CardMedia 渲染真实媒体元素,官方示例 ImgMediaCard.tsx:
<Card sx={{ maxWidth: 345 }}>
<CardMedia
component="img"
alt="green iguana"
height="140"
image="/static/images/cards/contemplative-reptile.jpg"
/>
{/* CardContent / CardActions 同上 */}
</Card>
源码中的判定逻辑值得注意(CardMedia.js):
MEDIA_COMPONENTS = ['video', 'audio', 'picture', 'iframe', 'img']:当component是这些媒体元素之一时(isMediaComponent),image会被转换为真实的src属性,并附带width: 100%;IMAGE_COMPONENTS = ['picture', 'img']:当component是picture/img时(isImageComponent),追加object-fit: cover,与背景图模式保持相同的裁切观感;- PropTypes 校验要求
children、image、src、component至少提供一个,否则会抛出 "Eitherchildren,image,srcorcomponentprop must be specified" 错误; - 文档特别提示:背景图模式下调用方必须显式指定
height(如sx={{ height: 140 }}),否则图片不可见——因为div没有固有高度。这也是MediaCard示例中height写在sx里的原因。
主要操作:CardActionArea 让整卡可交互
卡片经常需要让用户点击"整张卡片表面"来触发主操作(展开、跳转详情等)。用 CardActionArea 包裹内容即可实现,官方示例 ActionAreaCard.tsx:
import CardActionArea from '@mui/material/CardActionArea';
// 其余导入同 MediaCard
export default function ActionAreaCard() {
return (
<Card sx={{ maxWidth: 345 }}>
<CardActionArea>
<CardMedia
component="img"
height="140"
image="/static/images/cards/contemplative-reptile.jpg"
alt="green iguana"
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Lizard
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Lizards are a widespread group of squamate reptiles ...
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
}
从 CardActionArea.js 可以确认它的实现细节:
- 根节点基于
ButtonBase,因此自带onClick、键盘可达、focusVisible等完整的按钮语义,而不仅仅是"可点击的 div";display: block、width: 100%、border-radius: inherit让它铺满 Card 内部并继承 Card 的圆角(后者还附带注释指出是为修复 Safari 下的圆角继承问题)。 - 悬浮反馈通过独立覆盖层实现:
CardActionArea内部渲染一个FocusHighlight槽(span,绝对定位铺满、pointer-events: none、background-color: currentcolor)。鼠标悬停时该层 opacity 取theme.palette.action.hoverOpacity,键盘聚焦(focus visible)时取palette.action.focusOpacity,透明度变化使用theme.transitions.duration.short过渡;在@media (hover: none)(触屏设备)下悬浮层透明度固定为 0。 - 焦点环被设计为内缩(inset):源码注释明确写道 "Card sets overflow:hidden, which clips an outset ring"——因为 Card 根节点
overflow: hidden会裁掉外扩的焦点环,所以启用theme.focusVisible时焦点环向内缩进 1px,避免视觉被截断。
补充操作要与主操作区分离。 卡片还可以提供与主操作并列的补充动作,这些动作必须放在 CardActionArea 之外,以避免事件冒泡重叠。官方示例 MultiActionAreaCard.tsx 的结构是:
<Card sx={{ maxWidth: 345 }}>
<CardActionArea>
<CardMedia component="img" height="140" image="..." alt="green iguana" />
<CardContent>...</CardContent>
</CardActionArea>
{/* 补充操作放在 CardActionArea 之外,避免与主点击区域事件重叠 */}
<CardActions>
<Button size="small" color="primary">
Share
</Button>
</CardActions>
</Card>
UI 控件:底部媒体控制卡片
补充操作在卡片中通常以图标、文字和 UI 控件形式明确给出,并常规放置在卡片底部。MediaControlCard.tsx 展示了"音乐播放器"式卡片:左侧标题 + 控制按钮纵向排列,右侧是专辑封面。
const theme = useTheme();
<Card sx={{ display: 'flex' }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<CardContent sx={{ flex: '1 0 auto' }}>
<Typography component="div" variant="h5">
Live From Space
</Typography>
<Typography
variant="subtitle1"
component="div"
sx={{ color: 'text.secondary' }}
>
Mac Miller
</Typography>
</CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', pl: 1, pb: 1 }}>
<IconButton aria-label="previous">
{theme.direction === 'rtl' ? <SkipNextIcon /> : <SkipPreviousIcon />}
</IconButton>
<IconButton aria-label="play/pause">
<PlayArrowIcon sx={{ height: 38, width: 38 }} />
</IconButton>
<IconButton aria-label="next">
{theme.direction === 'rtl' ? <SkipPreviousIcon /> : <SkipNextIcon />}
</IconButton>
</Box>
</Box>
<CardMedia
component="img"
sx={{ width: 151 }}
image="/static/images/cards/live-from-space.jpg"
alt="Live from space album cover"
/>
</Card>
这个示例体现了两个要点:
- Card 根节点可以直接用
sx变成 flex 布局容器,配合CardContent的flex: '1 0 auto'让标题区撑满剩余高度,底部控制条自然沉底; - RTL 适配:上一首/下一首图标依据
theme.direction互换,这是 Material UI 对右到左语言的内置约定,控件类卡片应当遵循。
另外可以回顾 CardActions.js 的默认布局:display: flex + align-items: center + padding: 8,未禁用 spacing 时控件之间自动保持 8px 间距——因此底部放两个 size="small" 的 Button(如 MediaCard.tsx 中的 Share / Learn More)即可获得符合规范的间距。
Active 状态样式:data-active 属性 + &[data-active] 选择器
当卡片承担"选中项"语义(如选择列表)时,需要为 CardActionArea 定制激活态。官方推荐做法是:在 CardActionArea 上挂 data-active 属性,再用 &[data-active] 选择器应用样式。官方示例 SelectActionCard.tsx:
function SelectActionCard() {
const [selectedCard, setSelectedCard] = React.useState(0);
return (
<Box
sx={{
width: '100%',
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(min(200px, 100%), 1fr))',
gap: 2,
}}
>
{cards.map((card, index) => (
<Card key={card.id}>
<CardActionArea
onClick={() => setSelectedCard(index)}
data-active={selectedCard === index ? '' : undefined}
sx={{
height: '100%',
'&[data-active]': {
backgroundColor: 'action.selected',
'&:hover': {
backgroundColor: 'action.selectedHover',
},
},
}}
>
<CardContent sx={{ height: '100%' }}>
<Typography variant="h5" component="div">
{card.title}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{card.description}
</Typography>
</CardContent>
</CardActionArea>
</Card>
))}
</Box>
);
}
模式拆解:
data-active={selectedCard === index ? '' : undefined}:选中时属性存在(空字符串),未选中时undefined使属性从 DOM 上移除——属性"存在与否"本身就是状态载体;sx中的'&[data-active]'只对当前根元素带该属性时生效,选中态背景取主题 tokenaction.selected,叠加&:hover后变为action.selectedHover,与 Material 的选中语义色保持一致;- 之所以可行,是因为
CardActionArea根节点基于ButtonBase,会把未知属性透传到原生元素上(见 CardActionArea.js 中...other的展开),sx也经由同一根节点生效。
源码纵深:类名、插槽与多态能力
综合前面各组件源码,Card 家族的自定义点可以归纳如下,便于按槽位覆写样式:
| 组件 | 样式槽(utility classes) | 依据 |
|---|---|---|
Card |
root |
cardClasses.ts |
CardActionArea |
root、focusHighlight |
CardActionArea.js |
CardActions |
root、spacing |
CardActions.js |
CardContent |
root |
CardContent.js |
CardHeader |
root、avatar、action、content、title、subheader |
cardHeaderClasses.ts |
CardMedia |
root、media、img(后两者随 component 自动附加) |
CardMedia.js |
此外有两个源码级事实值得了解:
- Card 是多态组件(polymorphic)。Card.spec.tsx 的类型测试验证了
Card component="a" href="test"、Card component={CustomComponent}等用法下事件与自定义 prop 的类型推导——Card 透传 Paper 的多态能力,可以把整张卡片渲染成链接或任意自定义元素。 - 默认 prop 可通过
DefaultPropsProvider覆盖。所有组件内部都调用了useDefaultProps({ name: 'MuiCard' })(如 Card.js),意味着你可以在应用层用MuiCard/MuiCardActionArea等名字批量设置默认属性,而无需逐个传参。
小结
Material UI 的 Card 家族用"一个 Paper 容器 + 五个职责单一的配套组件"覆盖了卡片场景:CardContent/CardHeader 组织信息,CardMedia 负责媒体(注意 div 背景图与 component="img" 两种模式的取舍及 height 要求),CardActions 承载底部控件,CardActionArea 提供整卡交互与可访问的按钮语义,而 data-active + &[data-active] 则为选中态样式提供了标准化的扩展点。所有示例可直接对照 docs/data/material/components/cards 目录下的 demo 源文件(BasicCard.tsx、RecipeReviewCard.tsx、MediaCard.tsx、ImgMediaCard.tsx、ActionAreaCard.tsx、MultiActionAreaCard.tsx、MediaControlCard.tsx、SelectActionCard.tsx)与 packages/mui-material/src/Card 等源码目录进一步核对细节。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00