Material Design 3 Card web component — framework-agnostic, built with Lit.
An accessible M3 Card web component following the Material Design 3 card specifications. Supports elevated, filled, and outlined variants with media, header, content, and action slots. Works in Angular, React, Vue, Svelte, or plain HTML — no build step required.
- 3 Card Variants: Elevated, Filled, and Outlined
- Fully Interactive: Clickable cards with hover, focus, pressed, and dragged states
- Accessible: keyboard navigation support
- Framework Agnostic: Works with Angular, React, Vue, Svelte, or vanilla JavaScript
- Flexible Layout: Supports media, header, content, and action slots
- Customizable: CSS custom properties for theming
- TypeScript Support: Full type definitions included
- Material Design 3 Compliant: Follows official Material 3 card specifications
npm install @banegasn/m3-card
# or
pnpm add @banegasn/m3-card
# or
yarn add @banegasn/m3-card<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>M3 Card Demo</title>
<script type="module" src="https://cdn.jsdelivr.net/npm/@banegasn/m3-card/+esm"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@banegasn/m3-button/+esm"></script>
<style>
body { font-family: Roboto, sans-serif; padding: 32px; background: #fef7ff; }
.cards { display: flex; gap: 16px; flex-wrap: wrap; }
</style>
</head>
<body>
<div class="cards">
<m3-card variant="elevated" style="max-width:280px;">
<h3 slot="header" style="margin:0">Elevated Card</h3>
<p>Default card with subtle shadow elevation.</p>
<div slot="actions">
<m3-button variant="text">Cancel</m3-button>
<m3-button variant="filled">Confirm</m3-button>
</div>
</m3-card>
<m3-card variant="filled" style="max-width:280px;">
<h3 slot="header" style="margin:0">Filled Card</h3>
<p>Solid background for denser layouts.</p>
</m3-card>
<m3-card variant="outlined" clickable aria-label="View outlined card details" style="max-width:280px;">
<h3 slot="header" style="margin:0">Outlined Card</h3>
<p>Lightweight card with a clear border. Click me!</p>
</m3-card>
</div>
<script>
document.querySelectorAll('m3-card').forEach(card => {
card.addEventListener('card-click', (e) => console.log('Card clicked:', e.detail));
});
</script>
</body>
</html>import '@banegasn/m3-card';<!-- Simple elevated card (default) -->
<m3-card>
<h2 slot="header">Card Title</h2>
<p>This is a simple card with some content.</p>
</m3-card>
<!-- Filled card -->
<m3-card variant="filled">
<p>Filled card with solid background</p>
</m3-card>
<!-- Outlined card -->
<m3-card variant="outlined">
<p>Outlined card with border</p>
</m3-card><m3-card>
<img slot="media" src="image.jpg" alt="Description" />
<h2 slot="header">Beautiful Image</h2>
<p>Card with an image at the top</p>
</m3-card><m3-card>
<h2 slot="header">Action Card</h2>
<p>Card with action buttons at the bottom</p>
<div slot="actions">
<m3-button variant="text">Cancel</m3-button>
<m3-button variant="filled">Confirm</m3-button>
</div>
</m3-card><m3-card
clickable
aria-label="Navigate to details"
@card-click="${handleClick}"
>
<h2 slot="header">Interactive Card</h2>
<p>Click anywhere on this card to trigger an action</p>
</m3-card><m3-card variant="elevated" clickable width="fixed">
<img slot="media" src="product.jpg" alt="Product" />
<div slot="header">
<h3 style="margin: 0; font-size: 22px;">Product Title</h3>
<p style="margin: 4px 0 0; color: #666; font-size: 14px;">$99.99</p>
</div>
<p>
This is a detailed description of the product with all the
important information you need to know.
</p>
<div slot="actions">
<m3-button variant="text" icon="favorite">
<svg slot="icon" width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z"/>
</svg>
</m3-button>
<m3-button variant="text">Share</m3-button>
<m3-button variant="filled">Buy Now</m3-button>
</div>
</m3-card>| Property | Type | Default | Description |
|---|---|---|---|
variant |
'elevated' | 'filled' | 'outlined' |
'elevated' |
Card visual style variant |
clickable |
boolean |
false |
Makes the card interactive and clickable |
disabled |
boolean |
false |
Disables card interaction |
dragged |
boolean |
false |
Shows the card in dragged state |
width |
'auto' | 'full' | 'fixed' |
'auto' |
Controls card width behavior |
aria-label |
string | null |
null |
Accessibility label (required if clickable) |
role |
string | null |
null |
ARIA role (auto-set to 'button' if clickable) |
| Slot | Description |
|---|---|
| (default) | Main content area of the card |
media |
Media content (images, videos) at the top |
header |
Header content (title, subtitle) |
actions |
Action buttons at the bottom |
| Event | Detail | Description |
|---|---|---|
card-click |
{ variant, width } |
Fired when clickable card is clicked |
| Method | Description |
|---|---|
focus() |
Focuses the card (if clickable) |
blur() |
Removes focus from the card |
| Property | Default | Description |
|---|---|---|
--md-card-container-shape |
12px |
Border radius of the card |
--md-card-container-color |
varies | Background color (overrides default) |
--md-card-elevation |
varies | Box shadow elevation |
--md-card-width |
320px |
Fixed width (when width="fixed") |
--md-sys-color-surface-container-low |
#f7f2fa |
Elevated card background |
--md-sys-color-surface-container-highest |
#e6e0e9 |
Filled card background |
--md-sys-color-surface |
#fef7ff |
Outlined card background |
--md-sys-color-outline-variant |
#c9c5d0 |
Outlined card border |
--md-sys-color-on-surface |
#1d1b20 |
State layer overlay color |
Use when: You want subtle elevation with a shadow effect.
<m3-card variant="elevated">
<p>Elevated card with shadow</p>
</m3-card>- Background: Surface container low
- Elevation: Level 1 shadow
- Best for: Default cards, product cards
Use when: You want a solid background with more visual weight.
<m3-card variant="filled">
<p>Filled card with solid background</p>
</m3-card>- Background: Surface container highest
- Elevation: None
- Best for: Dense UIs, secondary content
Use when: You want a lightweight card with clear boundaries.
<m3-card variant="outlined">
<p>Outlined card with border</p>
</m3-card>- Background: Surface
- Border: Outline variant
- Best for: Lists, grouped content
When using clickable, always provide an aria-label:
<m3-card
clickable
aria-label="View product details for Wireless Headphones"
>
<!-- Card content -->
</m3-card>- Tab: Focus on clickable cards
- Enter or Space: Activate clickable cards
- Escape: Remove focus (standard browser behavior)
- Non-clickable cards: Regular div structure
- Clickable cards: Automatically assigned
role="button" - Disabled cards:
aria-disabled="true"
-
Use appropriate variants:
- Elevated: Default choice, good for most use cases
- Filled: Dense layouts, grouped content
- Outlined: Lists, minimal designs
-
Keep content organized:
- Use
headerslot for titles - Use
mediaslot for images/videos - Use
actionsslot for buttons - Keep main content in default slot
- Use
-
Make clickable cards accessible:
- Always provide
aria-labelfor clickable cards - Describe the action, not just the content
- Always provide
-
Responsive design:
- Use
width="full"for mobile layouts - Use
width="fixed"for desktop grids - Test on different screen sizes
- Use
-
Action buttons:
- Limit to 1-3 actions per card
- Use text buttons for secondary actions
- Use filled button for primary action
import '@banegasn/m3-card';
@Component({
selector: 'app-card-demo',
template: `
<m3-card [clickable]="true" (card-click)="handleClick($event)">
<h2 slot="header">{{ title }}</h2>
<p>{{ description }}</p>
</m3-card>
`
})
export class CardDemoComponent {
title = 'Card Title';
description = 'Card description';
handleClick(event: CustomEvent) {
console.log('Card clicked:', event.detail);
}
}import '@banegasn/m3-card';
function CardDemo() {
const handleClick = (event: any) => {
console.log('Card clicked:', event.detail);
};
return (
<m3-card clickable onCard-click={handleClick}>
<h2 slot="header">Card Title</h2>
<p>Card description</p>
</m3-card>
);
}<template>
<m3-card clickable @card-click="handleClick">
<h2 slot="header">{{ title }}</h2>
<p>{{ description }}</p>
</m3-card>
</template>
<script setup>
import '@banegasn/m3-card';
const title = 'Card Title';
const description = 'Card description';
const handleClick = (event) => {
console.log('Card clicked:', event.detail);
};
</script>Contributions are welcome! Please see the main repository for contribution guidelines.
MIT © banegasn
