Visual primitive

Card

<DomCard>

A visual-only frame for grouped content, styled by the skin-card theme recipe.

Playground

Try every prop live

Card playground

Cards are visual-only: skin-card provides the material recipe and spacing, while your markup decides the content.

Skin card

This primitive uses skin-card, so borders, shadows, gradients, and glass effects come from theme tokens.

Playground.vuevue
vue
<script setup>
import { reactive } from 'vue';
import { DomCard } from '@getdom/studio/vue';

const data = reactive({
	  "as": "div",
	  "padding": "md",
	  "interactive": false
	});
</script>

<template>
	<DomCard
		v-bind="data"
	/>
</template>

Demo

Basic content card

Use DomCard for grouped content that needs a consistent skin recipe.

Workspace

Design system audit

Review token usage and component coverage before the next release.

On track
Basic.vuevue
vue
<script setup>
import { DomButton, DomCard } from '../../../lib/vue';
</script>

<template>
	<DomCard class="max-w-sm">
		<div class="space-y-4">
			<div>
				<p class="text-sm font-medium text-muted-fg">Workspace</p>
				<h3 class="mt-1 text-xl font-semibold tracking-tight">Design system audit</h3>
				<p class="mt-2 text-sm leading-6 text-muted-fg">
					Review token usage and component coverage before the next release.
				</p>
			</div>
			<div class="flex items-center justify-between border-t border-border pt-4">
				<span class="rounded-full bg-success/15 px-2.5 py-1 text-xs font-medium text-success">On track</span>
				<DomButton variant="secondary" size="sm">Open</DomButton>
			</div>
		</div>
	</DomCard>
</template>

Variant

Aspect-ratio media cards

Use padding none when the card image owns the top edge, then keep the media area stable with aspect-ratio utilities.

Dashboard screen showing project health metrics and tasks.
Image Approved

Workspace dashboard hero

Product / 2400 x 1600

842 KB4:3 preview

Maya Chen / Hero

Colorful campaign artwork with layered product cards.
Image Approved

Launch banner background

Campaigns / 3200 x 1400

1.4 MB4:3 preview

Growth Studio / Launch

Workspace table with notes, laptops, and planning materials.
Image Review

Uploaded profile header

Uploads / 2100 x 900

734 KB4:3 preview

Alex Morgan / Profile

vue
<script setup>
import { DomBadge, DomButton, DomCard, DomStatusPill } from '../../../lib/vue';

const assets = [
	{
		id: 'dashboard-hero',
		title: 'Workspace dashboard hero',
		collection: 'Product',
		dimensions: '2400 x 1600',
		size: '842 KB',
		owner: 'Maya Chen',
		placement: 'Hero',
		status: 'Approved',
		tone: 'success',
		alt: 'Dashboard screen showing project health metrics and tasks.',
		src: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=900&h=675&q=80',
		position: 'center',
	},
	{
		id: 'launch-banner',
		title: 'Launch banner background',
		collection: 'Campaigns',
		dimensions: '3200 x 1400',
		size: '1.4 MB',
		owner: 'Growth Studio',
		placement: 'Launch',
		status: 'Approved',
		tone: 'success',
		alt: 'Colorful campaign artwork with layered product cards.',
		src: 'https://images.unsplash.com/photo-1557683316-973673baf926?auto=format&fit=crop&w=900&h=675&q=80',
		position: 'center',
	},
	{
		id: 'profile-header',
		title: 'Uploaded profile header',
		collection: 'Uploads',
		dimensions: '2100 x 900',
		size: '734 KB',
		owner: 'Alex Morgan',
		placement: 'Profile',
		status: 'Review',
		tone: 'warning',
		alt: 'Workspace table with notes, laptops, and planning materials.',
		src: 'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=900&h=675&q=80',
		position: 'center',
	},
];
</script>

<template>
	<div class="grid w-full max-w-5xl gap-4 sm:grid-cols-2 lg:grid-cols-3">
		<DomCard
			v-for="asset in assets"
			:key="asset.id"
			as="article"
			padding="none"
			class="min-w-0"
		>
			<div class="relative aspect-[4/3] overflow-hidden bg-secondary">
				<img
					:src="asset.src"
					:alt="asset.alt"
					class="absolute inset-0 size-full object-cover"
					:style="{ objectPosition: asset.position }"
					loading="lazy"
					decoding="async"
				>
				<div class="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-black/45 to-transparent"></div>
				<DomBadge class="absolute left-3 top-3 bg-black/55 text-white ring-white/20" size="sm">
					Image
				</DomBadge>
				<DomStatusPill class="absolute right-3 top-3" :tone="asset.tone" size="sm" :dot="false">
					{{ asset.status }}
				</DomStatusPill>
			</div>

			<div class="space-y-4 p-4">
				<div class="min-w-0">
					<h3 class="truncate text-sm font-semibold">{{ asset.title }}</h3>
					<p class="mt-1 truncate text-xs text-muted-fg">{{ asset.collection }} / {{ asset.dimensions }}</p>
				</div>

				<div class="flex flex-wrap items-center gap-2">
					<DomBadge size="sm" variant="outline">{{ asset.size }}</DomBadge>
					<DomBadge size="sm" variant="outline">4:3 preview</DomBadge>
				</div>

				<div class="flex items-center justify-between gap-3 border-t border-border pt-4">
					<p class="truncate text-xs text-muted-fg">{{ asset.owner }} / {{ asset.placement }}</p>
					<DomButton variant="secondary" size="sm">Preview</DomButton>
				</div>
			</div>
		</DomCard>
	</div>
</template>

Composition

Storefront card rails

Keep DomCard as a flexible frame: compose product, promo, background-image, and icon cards inside DomCardRail when the page needs horizontal browsing.

Store

Shop the devices and accessories that fit your day.

Fresh arrivals. See what is ready right now.

Aster Phone Pro

All out performance.

From GBP 999 or GBP 41.62/mo. for 24 mo.

Premium smartphone with a reflective screen.

New

StudioBook Neo

The magic of portable power at a surprising price.

From GBP 599 or GBP 49.91/mo. for 12 mo.

Open laptop balanced on a table.

New

Pride Edition Sport Loop

A vibrant woven band for every day.

From GBP 49 or GBP 4.08/mo. for 12 mo.

Smartwatch with a colorful watch face.

Aster Phone 17e

Feature stacked. Value packed.

From GBP 599 or GBP 24.95/mo. for 24 mo.

Smartphone held in one hand.

Help on hand. Choose the shopping support that suits you.

Store specialist

Shop one on one with a Specialist.

Online or in a store.

Friendly retail specialist smiling.

Shop with a Specialist over video.

Choose your next device in a guided one-way video session.

Phone video call interface held in a hand.

Today in store

Explore intelligence features.

Try it for yourself in a free hands-on session.

Today in store

Join free sessions at your local store.

Learn the latest features and go further with every device.

The Store difference. More reasons to shop with confidence.

Trade in your current device. Get credit toward a new one.
Pay in full or pay over time. Your choice.
Make them yours. Engrave emoji, names, and numbers. For free.
Enjoy two-hour delivery free delivery, or easy pickup.
Get a personalized shopping experience in the Store app.

Accessories. Essentials that pair perfectly with your favorite devices.

Colorful headphones and tech accessories.
Here and wow.

The accessories you love. In a fresh mix of colors.

Bright phone case on a neutral background.
+

New

Aster Phone Silicone Case with MagSafe - Guava

GBP 49.00

Bright strap accessory photographed on fabric.
+

New

Crossbody Strap - Bright Guava

GBP 59.00

White charging cable on a clean background.

Magnetic Fast Charger (1 m)

GBP 39.00

Clear phone case with a phone inside.
+

New

Aster Phone 17e Clear Case with MagSafe

GBP 49.00

vue
<script setup>
import { DomCard, DomCardRail } from '../../../lib/vue';

const categoryItems = [
	{
		id: 'laptop',
		label: 'Laptops',
		image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=320&h=220&q=80',
		alt: 'Silver laptop on a desk.',
	},
	{
		id: 'phone',
		label: 'Phones',
		image: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?auto=format&fit=crop&w=320&h=220&q=80',
		alt: 'Smartphone shown at an angle.',
	},
	{
		id: 'tablet',
		label: 'Tablets',
		image: 'https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?auto=format&fit=crop&w=320&h=220&q=80',
		alt: 'Tablet with a bright screen.',
	},
	{
		id: 'watch',
		label: 'Watches',
		image: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?auto=format&fit=crop&w=320&h=220&q=80',
		alt: 'Modern smartwatch face.',
	},
	{
		id: 'audio',
		label: 'Audio',
		image: 'https://images.unsplash.com/photo-1606220945770-b5b6c2c55bf1?auto=format&fit=crop&w=320&h=220&q=80',
		alt: 'Wireless earbuds in a charging case.',
	},
	{
		id: 'accessories',
		label: 'Accessories',
		image: 'https://images.unsplash.com/photo-1583394838336-acd977736f90?auto=format&fit=crop&w=320&h=220&q=80',
		alt: 'Over-ear headphones on a clean surface.',
	},
];

const latestCards = [
	{
		id: 'phone-pro',
		eyebrow: '',
		title: 'Aster Phone Pro',
		subtitle: 'All out performance.',
		price: 'From GBP 999 or GBP 41.62/mo. for 24 mo.',
		image: 'https://images.unsplash.com/photo-1592899677977-9c10ca588bbd?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'Premium smartphone with a reflective screen.',
		tone: 'dark',
	},
	{
		id: 'laptop-neo',
		eyebrow: 'New',
		title: 'StudioBook Neo',
		subtitle: 'The magic of portable power at a surprising price.',
		price: 'From GBP 599 or GBP 49.91/mo. for 12 mo.',
		image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'Open laptop balanced on a table.',
		tone: 'light',
	},
	{
		id: 'watch-band',
		eyebrow: 'New',
		title: 'Pride Edition Sport Loop',
		subtitle: 'A vibrant woven band for every day.',
		price: 'From GBP 49 or GBP 4.08/mo. for 12 mo.',
		image: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'Smartwatch with a colorful watch face.',
		tone: 'light',
	},
	{
		id: 'phone-lite',
		eyebrow: '',
		title: 'Aster Phone 17e',
		subtitle: 'Feature stacked. Value packed.',
		price: 'From GBP 599 or GBP 24.95/mo. for 24 mo.',
		image: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'Smartphone held in one hand.',
		tone: 'light',
	},
];

const supportCards = [
	{
		id: 'specialist',
		eyebrow: 'Store specialist',
		title: 'Shop one on one with a Specialist.',
		subtitle: 'Online or in a store.',
		image: 'https://images.unsplash.com/photo-1580894732444-8ecded7900cd?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'Friendly retail specialist smiling.',
		variant: 'portrait',
	},
	{
		id: 'video',
		eyebrow: '',
		title: 'Shop with a Specialist over video.',
		subtitle: 'Choose your next device in a guided one-way video session.',
		image: 'https://images.unsplash.com/photo-1616469829581-73993eb86b02?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'Phone video call interface held in a hand.',
		variant: 'device',
	},
	{
		id: 'session',
		eyebrow: 'Today in store',
		title: 'Explore intelligence features.',
		subtitle: 'Try it for yourself in a free hands-on session.',
		image: 'https://images.unsplash.com/photo-1556761175-b413da4baf72?auto=format&fit=crop&w=1100&h=900&q=80',
		alt: 'People learning together in a bright retail space.',
		variant: 'photo',
	},
	{
		id: 'learn',
		eyebrow: 'Today in store',
		title: 'Join free sessions at your local store.',
		subtitle: 'Learn the latest features and go further with every device.',
		image: 'https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1100&h=900&q=80',
		alt: 'Small group collaborating around a table.',
		variant: 'photo',
	},
];

const differenceCards = [
	{
		id: 'trade-in',
		color: 'text-primary',
		icon: 'M7 7h10v10H7V7Zm-2 3H3v7a2 2 0 0 0 2 2h7v-2H5v-7Zm7-5h7v7h2V5a2 2 0 0 0-2-2h-7v2Zm-2 5h4v4h-4v-4Z',
		before: 'Trade in your current device.',
		accent: 'Get credit',
		after: 'toward a new one.',
	},
	{
		id: 'payments',
		color: 'text-success',
		icon: 'M4 7h16v10H4V7Zm0 3h16M7 14h4',
		before: 'Pay in full or',
		accent: 'pay over time.',
		after: 'Your choice.',
	},
	{
		id: 'engraving',
		color: 'text-fuchsia-500',
		icon: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM9 10h.01M15 10h.01M8.5 14.5c1.7 1.5 5.3 1.5 7 0',
		before: 'Make them yours.',
		accent: 'Engrave emoji, names, and numbers.',
		after: 'For free.',
	},
	{
		id: 'delivery',
		color: 'text-emerald-600',
		icon: 'M3 7h11v9H3V7Zm11 3h3l3 3v3h-6v-6ZM7 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm10 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z',
		before: 'Enjoy two-hour delivery',
		accent: 'free delivery, or easy pickup.',
		after: '',
	},
	{
		id: 'app',
		color: 'text-blue-600',
		icon: 'M8 4h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm3 4v8M14 8v8M10 11h5',
		before: 'Get a personalized',
		accent: 'shopping experience',
		after: 'in the Store app.',
	},
];

const accessoryCards = [
	{
		id: 'bundle',
		type: 'feature',
		title: 'Here and wow.',
		subtitle: 'The accessories you love. In a fresh mix of colors.',
		image: 'https://images.unsplash.com/photo-1583394838336-acd977736f90?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'Colorful headphones and tech accessories.',
	},
	{
		id: 'case',
		eyebrow: 'New',
		title: 'Aster Phone Silicone Case with MagSafe - Guava',
		price: 'GBP 49.00',
		image: 'https://images.unsplash.com/photo-1602524816200-77111955970f?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'Bright phone case on a neutral background.',
		swatches: ['bg-rose-400', 'bg-orange-100', 'bg-slate-300', 'bg-stone-500', 'bg-zinc-700'],
	},
	{
		id: 'strap',
		eyebrow: 'New',
		title: 'Crossbody Strap - Bright Guava',
		price: 'GBP 59.00',
		image: 'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'Bright strap accessory photographed on fabric.',
		swatches: ['bg-rose-100', 'bg-rose-400', 'bg-lime-200', 'bg-slate-300', 'bg-stone-300'],
	},
	{
		id: 'charger',
		eyebrow: '',
		title: 'Magnetic Fast Charger (1 m)',
		price: 'GBP 39.00',
		image: 'https://images.unsplash.com/photo-1615526675159-e248c3021d3f?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'White charging cable on a clean background.',
		swatches: [],
	},
	{
		id: 'clear-case',
		eyebrow: 'New',
		title: 'Aster Phone 17e Clear Case with MagSafe',
		price: 'GBP 49.00',
		image: 'https://images.unsplash.com/photo-1601784551446-20c9e07cdbdb?auto=format&fit=crop&w=900&h=900&q=80',
		alt: 'Clear phone case with a phone inside.',
		swatches: ['bg-rose-100', 'bg-white'],
	},
];
</script>

<template>
	<div class="store-example min-w-0 max-w-full rounded-3xl border border-border bg-secondary/70 p-4 text-canvas-fg shadow-xl shadow-black/5 sm:w-full sm:p-6 lg:p-8">
		<header class="flex flex-col gap-4 pb-8 lg:flex-row lg:items-end lg:justify-between">
			<div>
				<p class="text-sm font-semibold text-muted-fg">Store</p>
				<h3 class="mt-2 max-w-3xl break-words text-3xl font-semibold leading-tight tracking-normal text-canvas-fg sm:text-4xl">
					Shop the devices and accessories that fit your day.
				</h3>
			</div>
			<div class="grid gap-2 text-sm text-muted-fg">
				<a href="#specialist" class="font-semibold text-primary">Ask a Specialist</a>
				<a href="#visit" class="font-semibold text-primary">Find a store</a>
			</div>
		</header>

		<DomCardRail class="store-rail" aria-label="Shop product categories" style="--dom-card-rail-gap: 1.5rem;">
			<a
				v-for="category in categoryItems"
				:key="category.id"
				href="#"
				class="store-category"
			>
				<img :src="category.image" :alt="category.alt" loading="lazy" decoding="async">
				<span>{{ category.label }}</span>
			</a>
		</DomCardRail>

		<DomCardRail class="store-rail mt-12" aria-label="Latest products" style="--dom-card-rail-gap: 1.25rem;">
			<template #header>
				<h4 class="store-section-title">Fresh arrivals. <span>See what is ready right now.</span></h4>
			</template>

			<DomCard
				v-for="card in latestCards"
				:key="card.id"
				as="article"
				padding="none"
				class="store-promo-card"
				:class="card.tone === 'dark' && 'store-promo-card--dark'"
			>
				<div class="store-promo-copy">
					<p class="store-eyebrow">{{ card.eyebrow }}</p>
					<h5>{{ card.title }}</h5>
					<p class="store-subtitle">{{ card.subtitle }}</p>
					<p class="store-price">{{ card.price }}</p>
				</div>
				<img class="store-promo-image" :src="card.image" :alt="card.alt" loading="lazy" decoding="async">
			</DomCard>
		</DomCardRail>

		<DomCardRail id="specialist" class="store-rail mt-12" aria-label="Shopping help" style="--dom-card-rail-gap: 1.25rem;">
			<template #header>
				<h4 class="store-section-title">Help on hand. <span>Choose the shopping support that suits you.</span></h4>
			</template>

			<DomCard
				v-for="card in supportCards"
				:key="card.id"
				as="article"
				padding="none"
				class="store-service-card"
				:class="`store-service-card--${card.variant}`"
				:style="{ '--store-card-image': `url(${card.image})` }"
			>
				<div class="store-service-content">
					<p class="store-eyebrow">{{ card.eyebrow }}</p>
					<h5>{{ card.title }}</h5>
					<p>{{ card.subtitle }}</p>
				</div>
				<img
					v-if="card.variant !== 'photo'"
					class="store-service-image"
					:src="card.image"
					:alt="card.alt"
					loading="lazy"
					decoding="async"
				>
			</DomCard>
		</DomCardRail>

		<DomCardRail class="store-rail mt-12" aria-label="Store difference" style="--dom-card-rail-gap: 1rem;">
			<template #header>
				<h4 class="store-section-title">The Store difference. <span>More reasons to shop with confidence.</span></h4>
			</template>

			<DomCard
				v-for="item in differenceCards"
				:key="item.id"
				as="article"
				padding="lg"
				class="store-difference-card"
			>
				<svg viewBox="0 0 24 24" class="size-9" :class="item.color" fill="none" aria-hidden="true">
					<path :d="item.icon" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
				</svg>
				<h5>
					{{ item.before }}
					<span :class="item.color">{{ item.accent }}</span>
					{{ item.after }}
				</h5>
			</DomCard>
		</DomCardRail>

		<DomCardRail id="visit" class="store-rail mt-12" aria-label="Accessories" style="--dom-card-rail-gap: 1.25rem;">
			<template #header>
				<h4 class="store-section-title">Accessories. <span>Essentials that pair perfectly with your favorite devices.</span></h4>
			</template>

			<DomCard
				v-for="item in accessoryCards"
				:key="item.id"
				as="article"
				padding="none"
				class="store-accessory-card"
				:class="item.type === 'feature' && 'store-accessory-card--feature'"
			>
				<div class="store-accessory-media">
					<img :src="item.image" :alt="item.alt" loading="lazy" decoding="async">
				</div>

				<div class="store-accessory-copy">
					<div v-if="item.type === 'feature'">
						<h5>{{ item.title }}</h5>
						<p>{{ item.subtitle }}</p>
					</div>
					<div v-else class="grid h-full gap-4">
						<div class="store-swatches" aria-label="Available colors">
							<span
								v-for="swatch in item.swatches"
								:key="swatch"
								class="store-swatch"
								:class="swatch"
							></span>
							<span v-if="item.swatches.length" class="text-xs text-muted-fg">+</span>
						</div>
						<div>
							<p class="store-eyebrow">{{ item.eyebrow }}</p>
							<h5>{{ item.title }}</h5>
						</div>
						<p class="self-end text-sm text-muted-fg">{{ item.price }}</p>
					</div>
				</div>
			</DomCard>
		</DomCardRail>

	</div>
</template>

<style scoped>
.store-example {
	--store-card-muted: var(--muted-fg);
}

.store-rail {
	--dom-card-rail-edge: 0.25rem;
}

.store-section-title {
	max-width: 54rem;
	color: var(--canvas-fg);
	font-size: 1.45rem;
	font-weight: 700;
	letter-spacing: 0;
	line-height: 1.15;
}

.store-section-title span {
	color: var(--muted-fg);
}

.store-category {
	display: grid;
	width: 7.5rem;
	justify-items: center;
	gap: 0.75rem;
	color: var(--canvas-fg);
	font-weight: 650;
	text-align: center;
	text-decoration: none;
}

.store-category img {
	width: 7rem;
	height: 5rem;
	border-radius: var(--radius-xl);
	object-fit: cover;
	box-shadow: var(--shadow-sm);
}

.store-promo-card {
	--store-card-muted: var(--muted-fg);
	width: min(28rem, 78vw);
	height: 30rem;
	display: grid;
	grid-template-rows: auto minmax(0, 1fr);
}

.store-promo-card--dark {
	--skin-card-bg: oklch(0.12 0 0);
	--skin-card-border: transparent;
	--skin-card-fg: oklch(0.98 0 0);
	--store-card-muted: oklch(0.84 0 0);
}

.store-promo-copy {
	position: relative;
	z-index: 1;
	padding: 2rem 2rem 0;
	color: var(--skin-card-fg, var(--canvas-fg));
}

.store-eyebrow {
	min-height: 1.1rem;
	color: var(--destructive);
	font-size: 0.72rem;
	font-weight: 800;
	letter-spacing: 0.03em;
	text-transform: uppercase;
}

.store-promo-copy h5,
.store-service-content h5,
.store-accessory-copy h5 {
	margin-top: 0.4rem;
	font-size: 1.65rem;
	font-weight: 750;
	letter-spacing: 0;
	line-height: 1.08;
}

.store-subtitle,
.store-price,
.store-service-content p,
.store-accessory-copy p {
	margin-top: 0.65rem;
	color: var(--store-card-muted, var(--muted-fg));
	font-size: 0.95rem;
	line-height: 1.35;
}

.store-price {
	font-weight: 500;
}

.store-promo-image {
	align-self: end;
	width: 100%;
	height: 19rem;
	object-fit: cover;
	object-position: center;
}

.store-service-card {
	width: min(30rem, 82vw);
	height: 28rem;
	display: grid;
	grid-template-rows: auto minmax(0, 1fr);
}

.store-service-card--photo {
	--skin-card-border: transparent;
	background-image:
		linear-gradient(180deg, color-mix(in oklab, var(--canvas) 94%, transparent), color-mix(in oklab, var(--canvas) 18%, transparent)),
		var(--store-card-image);
	background-position: center;
	background-size: cover;
}

.store-service-card--photo .store-service-content {
	max-width: 20rem;
}

.store-service-content {
	position: relative;
	z-index: 1;
	padding: 2rem 2rem 0;
}

.store-service-image {
	align-self: end;
	width: 100%;
	height: 18rem;
	object-fit: cover;
	object-position: center top;
}

.store-service-card--device .store-service-image {
	object-fit: contain;
	padding-inline: 2rem;
}

.store-difference-card {
	width: min(20rem, 76vw);
	height: 12rem;
	display: grid;
	align-content: start;
	gap: 1rem;
}

.store-difference-card h5 {
	font-size: 1.35rem;
	font-weight: 750;
	letter-spacing: 0;
	line-height: 1.13;
}

.store-accessory-card {
	width: min(20rem, 78vw);
	height: 30rem;
	display: grid;
	grid-template-rows: minmax(15rem, 1fr) 13rem;
}

.store-accessory-card--feature {
	width: min(25rem, 82vw);
}

.store-accessory-media {
	display: grid;
	place-items: end center;
	min-height: 0;
	padding: 2rem 1.5rem 0;
}

.store-accessory-media img {
	max-width: 100%;
	max-height: 17rem;
	border-radius: var(--radius-xl);
	object-fit: cover;
}

.store-accessory-card--feature .store-accessory-media img {
	width: 100%;
	height: 18rem;
}

.store-accessory-copy {
	display: grid;
	min-height: 0;
	padding: 1.35rem 1.5rem 1.5rem;
}

.store-accessory-copy h5 {
	font-size: 1rem;
	line-height: 1.18;
}

.store-accessory-card--feature .store-accessory-copy {
	align-content: start;
	order: -1;
	padding: 2rem 2rem 0;
}

.store-accessory-card--feature .store-accessory-copy h5 {
	font-size: 1.55rem;
}

.store-swatches {
	display: flex;
	min-height: 1rem;
	align-items: center;
	justify-content: center;
	gap: 0.35rem;
}

.store-swatch {
	width: 0.7rem;
	height: 0.7rem;
	border: 1px solid var(--border);
	border-radius: 999px;
	box-shadow: var(--shadow-xs);
}

@media (max-width: 640px) {
	.store-example {
		border-radius: var(--radius-2xl);
	}

	.store-promo-copy,
	.store-service-content,
	.store-accessory-copy,
	.store-accessory-card--feature .store-accessory-copy {
		padding-inline: 1.35rem;
	}

	.store-promo-card,
	.store-service-card,
	.store-accessory-card {
		height: 27rem;
	}
}

@media (min-width: 768px) {
	.store-section-title {
		font-size: 2rem;
	}
}
</style>

Demo

Token-driven glass

Glass is a skin recipe, so it can be applied by changing CSS variables rather than component props.

Today

42,860

Monthly active sessions

Glass.vuevue
vue
<script setup>
import { DomButton, DomCard } from '../../../lib/vue';
</script>

<template>
	<div class="grid w-full max-w-2xl gap-4 sm:grid-cols-[1fr_0.8fr]">
		<DomCard padding="lg" class="min-h-64">
			<div class="flex h-full flex-col justify-between gap-8">
				<div>
					<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Today</p>
					<h3 class="mt-3 text-3xl font-semibold tracking-tight">42,860</h3>
					<p class="mt-2 text-sm text-muted-fg">Monthly active sessions</p>
				</div>
				<div class="grid grid-cols-6 items-end gap-1.5">
					<span v-for="height in [36, 54, 44, 72, 62, 92]" :key="height" class="rounded-t-lg bg-primary/80" :style="{ height: `${height}px` }"></span>
				</div>
			</div>
		</DomCard>

		<DomCard interactive as="button" class="w-full text-left">
			<div class="space-y-5">
				<div class="flex items-center gap-3">
					<img
						src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=96&h=96&q=80"
						alt=""
						class="size-12 rounded-full object-cover ring-2 ring-canvas"
					>
					<div>
						<p class="font-semibold">Avery Stone</p>
						<p class="text-sm text-muted-fg">Product lead</p>
					</div>
				</div>
				<p class="text-sm leading-6 text-muted-fg">
					Glass cards come from skin tokens, so theme contrast stays predictable.
				</p>
				<DomButton variant="primary" size="sm">Message</DomButton>
			</div>
		</DomCard>
	</div>
</template>

Reference

Props

Control props

NameTypeTSDefaultDescription
asstringstring'div'The HTML element or component to render. Use 'section', 'article', or a router-link when useful.
padding'none' | 'sm' | 'md' | 'lg'string'md'Internal spacing. Use none when the card owns its own internal layout.
interactivebooleanbooleanfalseAdd a restrained hover lift for clickable cards.

Auto-generated from Card.props and inline _edit hints.

Slots

NameScopeDescription
#(default)Card content.