news 2026/9/6 21:39:10

Material UI Card 组件族实战:从基础卡片、媒体展示到整卡交互与 Active 状态样式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Material UI Card 组件族实战:从基础卡片、媒体展示到整卡交互与 Active 状态样式

Material UI Card 组件族实战:从基础卡片、媒体展示到整卡交互与 Active 状态样式

【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui

本篇技术指南基于 Material UI 官方 Card 文档(cards.md),系统讲解 Card 及其配套组件CardContentCardHeaderCardMediaCardActionsCardActionArea的完整用法。读完本文,你将能够:构建基础/描边卡片、用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

最小可用的卡片只需CardCardContent

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> ); }

结合源码可以确认几个默认行为:

  1. Card 是 Paper 的特化。Card.js 中根节点由styled(Paper)派生,仅追加一条overflow: 'hidden'样式(这也是后续 CardMedia 图片被裁切、焦点环需要改为内缩的原因)。因此 Card 继承 Paper 的variantelevationsquarecomponent等全部属性。
  2. raised属性等价于 elevation 8。从 Card.js 可见,raised={true}时向 Paper 传入elevation={8};其 PropTypes 还内置了一条校验:raisedvariant="outlined"同时使用时会告警"组合无效"。
  3. 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> ); }

这里有几个可复用的交互技巧:

  • ExpandMorestyled(IconButton)做条件旋转:箭头是否展开通过expand布尔 prop 传入,用variants声明rotate(0deg)/rotate(180deg)两态,过渡时长取自theme.transitions.duration.shortest
  • Collapse in={expanded} timeout="auto" unmountOnExit:展开动画时长由组件自动计算,动画结束后从 DOM 卸载,避免隐藏内容仍占用布局与可访问性树;
  • CardActions disableSpacingdisableSpacing关闭卡片底部按钮组默认的 8px 内边距与控件间距,让图标按钮更紧凑。这一点可以从 CardActions.js 得到印证:根节点默认display: flex; align-items: center; padding: 8,且只有当disableSpacingfalse时才应用spacing类(给后续兄弟元素加margin-left: 8);
  • CardHeader的插槽化结构:从 cardHeaderClasses.ts 可见,CardHeader 暴露rootavataractioncontenttitlesubheader六个样式槽,分别对应示例中的avataractiontitlesubheader属性,需要精细定制头部时可按这些类名覆写。

媒体展示: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。当传入imagecomponent仍是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']:当componentpicture/img时(isImageComponent),追加object-fit: cover,与背景图模式保持相同的裁切观感;
  • PropTypes 校验要求childrenimagesrccomponent至少提供一个,否则会抛出 "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: blockwidth: 100%border-radius: inherit让它铺满 Card 内部并继承 Card 的圆角(后者还附带注释指出是为修复 Safari 下的圆角继承问题)。
  • 悬浮反馈通过独立覆盖层实现CardActionArea内部渲染一个FocusHighlight槽(span,绝对定位铺满、pointer-events: nonebackground-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>

这个示例体现了两个要点:

  1. Card 根节点可以直接用sx变成 flex 布局容器,配合CardContentflex: '1 0 auto'让标题区撑满剩余高度,底部控制条自然沉底;
  2. 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)} contenteditable="false">【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 21:38:15

如何制作AI主题PPT?从三幕结构到AI工具实战

简介&#xff1a;这份PPT围绕“人工智能现状与未来”展开&#xff0c;系统梳理了AI从1956年达特茅斯会议诞生至今的发展脉络&#xff0c;涵盖孕育期、低谷期、快速发展期等阶段&#xff0c;并解读机器学习、深度学习、知识图谱等核心技术&#xff0c;以及符号学派、联结学派、行…

作者头像 李华
网站建设 2026/9/6 21:36:52

WeKnora 本地部署:5 步从空机器到能问答的文档知识库

WeKnora 本地部署&#xff1a;5 步从空机器到能问答的文档知识库 【免费下载链接】WeKnora Open-source LLM knowledge platform: turn raw documents into a queryable RAG, an autonomous reasoning agent, and a self-maintaining Wiki. 项目地址: https://gitcode.com/Gi…

作者头像 李华
网站建设 2026/9/6 21:33:46

Python三级考级全攻略:核心考点、备考路线与上机实操指南

简介&#xff1a;中国电子学会青少年软件编程等级考试Python三级备考资料&#xff0c;面向参加等级考试的青少年及辅导教师&#xff0c;系统梳理三级涉及的Python语言基础核心知识与常见题型。文档重点覆盖表达式与运算符、变量与数据类型、条件判断与循环控制、赋值语句、变量…

作者头像 李华
网站建设 2026/9/6 21:30:23

猫抓 cat-catch 视频嗅探与下载:3 步拿到网页视频的完整指南

猫抓 cat-catch 视频嗅探与下载&#xff1a;3 步拿到网页视频的完整指南 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓&#xff08;cat-catc…

作者头像 李华