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 及其配套组件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)} 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),仅供参考