17 / 18
All 18 in 18 Tailwind CSS Product Cards
- 01Tailwind Product Card with Hover-Reveal Add to Cart
- 02Pure-Tailwind 3D Flip Product Card with Spec Back
- 03Tailwind Contained Image Zoom Product Card
- 04Tailwind Mouse-Tracking 3D Tilt Product Card
- 05Tailwind Slide-Up Details Panel Product Card
- 06Tailwind Minimalist eCommerce Product Card
- 07Tailwind Glassmorphism Product Card
- 08Tailwind Dark-Mode Gadget Product Card with Accent Glow
- 09Tailwind Neon Glowing Conic-Border Product Card
- 10Tailwind Material-Design Elevation Product Card
- 11Tailwind Horizontal Product Card with Container Queries
- 12Tailwind Auto-Fit Product Catalog Grid
- 13Tailwind Sale Ribbon & Coupon Badge Product Card
- 14Tailwind Sneaker Card with Colourway & Size Picker
- 15Tailwind Digital Download Product Card
- 16Tailwind Food Delivery Card with Quantity Stepper
- 17Tailwind Real Estate Listing Card with Favourite Toggle
- 18Tailwind Subscription Pricing Card with Billing Switch
Tailwind Real Estate Listing Card with Favourite Toggle
A property card led by price, with a status pill, a beds/baths/sqft feature row and a favourite heart that is a real checkbox — no script. peer-checked: fills and pops the heart, and because the same checkbox is read by a peer-checked:opacity-100 pill, the saved state is text as well as colour.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tpc-17 w-full min-h-svh grid place-items-center bg-tpc17-bg font-tpc17-sans px-5 py-12" aria-label="Real estate listing card demo">
<article class="w-[min(350px,100%)] rounded-[18px] overflow-hidden bg-tpc17-card shadow-[0_16px_40px_-22px_oklch(0.25_0.02_240/0.4)]">
<div class="relative aspect-[16/10] bg-linear-[135deg,var(--color-tpc17-ph),var(--color-tpc17-ph2)] bg-[url('https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=800&h=500&fit=crop&q=70')] bg-cover bg-center">
<div class="absolute top-3 left-3 flex items-center gap-2">
<span class="px-3 py-1.25 rounded-full bg-tpc17-acc text-tpc17-onacc text-[11px] font-bold tracking-[0.04em]">For Sale</span>
<span aria-hidden="true" class="tpc17-saved px-3 py-1.25 rounded-full bg-tpc17-card text-tpc17-fav text-[11px] font-bold tracking-[0.04em] transition-opacity duration-200 motion-reduce:transition-none">Saved</span>
</div>
<input type="checkbox" id="tpc17-fav" class="peer sr-only">
<label for="tpc17-fav" aria-label="Save to favourites" class="absolute top-3 right-3 grid place-items-center size-11 rounded-full cursor-pointer bg-tpc17-card/92 text-[20px] leading-none text-tpc17-heart shadow-[0_4px_12px_-4px_oklch(0.2_0.02_240/0.35)] transition-[color,scale] duration-200 hover:text-tpc17-fav peer-checked:text-tpc17-fav peer-checked:scale-110 peer-focus-visible:outline-3 peer-focus-visible:outline-tpc17-acc peer-focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:peer-checked:scale-100"><span aria-hidden="true">♥</span></label>
</div>
<div class="px-5 pt-4.5 pb-5">
<p class="text-[24px] font-extrabold text-tpc17-ink tabular-nums">$685,000</p>
<h3 class="mt-1 text-[16px] font-semibold text-tpc17-ink2">12 Larch Grove, Portland</h3>
<p class="mt-0.5 text-[13px] text-tpc17-mut">Detached · 2 storeys</p>
<ul class="mt-4 pt-4 border-t border-tpc17-edge flex justify-between list-none">
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">3</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Beds</span></li>
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">2</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Baths</span></li>
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">1,840</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Sq Ft</span></li>
</ul>
</div>
</article>
</section><section class="tpc-17 w-full min-h-svh grid place-items-center bg-tpc17-bg font-tpc17-sans px-5 py-12" aria-label="Real estate listing card demo">
<article class="w-[min(350px,100%)] rounded-[18px] overflow-hidden bg-tpc17-card shadow-[0_16px_40px_-22px_oklch(0.25_0.02_240/0.4)]">
<div class="relative aspect-[16/10] bg-linear-[135deg,var(--color-tpc17-ph),var(--color-tpc17-ph2)] bg-[url('https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=800&h=500&fit=crop&q=70')] bg-cover bg-center">
<div class="absolute top-3 left-3 flex items-center gap-2">
<span class="px-3 py-1.25 rounded-full bg-tpc17-acc text-tpc17-onacc text-[11px] font-bold tracking-[0.04em]">For Sale</span>
<span aria-hidden="true" class="tpc17-saved px-3 py-1.25 rounded-full bg-tpc17-card text-tpc17-fav text-[11px] font-bold tracking-[0.04em] transition-opacity duration-200 motion-reduce:transition-none">Saved</span>
</div>
<input type="checkbox" id="tpc17-fav" class="peer sr-only">
<label for="tpc17-fav" aria-label="Save to favourites" class="absolute top-3 right-3 grid place-items-center size-11 rounded-full cursor-pointer bg-tpc17-card/92 text-[20px] leading-none text-tpc17-heart shadow-[0_4px_12px_-4px_oklch(0.2_0.02_240/0.35)] transition-[color,scale] duration-200 hover:text-tpc17-fav peer-checked:text-tpc17-fav peer-checked:scale-110 peer-focus-visible:outline-3 peer-focus-visible:outline-tpc17-acc peer-focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:peer-checked:scale-100"><span aria-hidden="true">♥</span></label>
</div>
<div class="px-5 pt-4.5 pb-5">
<p class="text-[24px] font-extrabold text-tpc17-ink tabular-nums">$685,000</p>
<h3 class="mt-1 text-[16px] font-semibold text-tpc17-ink2">12 Larch Grove, Portland</h3>
<p class="mt-0.5 text-[13px] text-tpc17-mut">Detached · 2 storeys</p>
<ul class="mt-4 pt-4 border-t border-tpc17-edge flex justify-between list-none">
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">3</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Beds</span></li>
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">2</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Baths</span></li>
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">1,840</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Sq Ft</span></li>
</ul>
</div>
</article>
</section>@import "tailwindcss";
@theme {
--color-tpc17-bg: #eef1f4;
--color-tpc17-bg: oklch(0.95 0.006 240);
--color-tpc17-card: #ffffff;
--color-tpc17-card: oklch(1 0 0);
--color-tpc17-ink: #101828;
--color-tpc17-ink: oklch(0.23 0.03 260);
--color-tpc17-ink2: #344054;
--color-tpc17-ink2: oklch(0.4 0.03 260);
--color-tpc17-mut: #5b6678;
--color-tpc17-mut: oklch(0.5 0.025 260);
--color-tpc17-edge: #e4e8ee;
--color-tpc17-edge: oklch(0.93 0.008 250);
--color-tpc17-ph: #d7dfe8;
--color-tpc17-ph: oklch(0.89 0.015 240);
--color-tpc17-ph2: #c3cdda;
--color-tpc17-ph2: oklch(0.84 0.022 245);
--color-tpc17-heart: #9aa5b4;
--color-tpc17-heart: oklch(0.71 0.018 250);
--color-tpc17-fav: #c8324a;
--color-tpc17-fav: oklch(0.55 0.18 15);
--color-tpc17-acc: #16688f;
--color-tpc17-acc: oklch(0.47 0.11 225);
--color-tpc17-onacc: #ffffff;
--color-tpc17-onacc: oklch(1 0 0);
--font-tpc17-sans: 'Manrope', system-ui, sans-serif;
}
.tpc-17 {
width: 100%;
min-height: 100svh;
}
.tpc-17 ::selection {
background: var(--color-tpc17-acc);
color: #fff;
}
/* The 'Saved' pill sits BEFORE the checkbox in source order, and peer-* only looks
forward — so one :has() rule owns the pill's visibility outright. Declaring it here
rather than fighting an opacity-0 utility also sidesteps the layering question:
this rule is unlayered, so nothing in @layer utilities can override it. */
.tpc-17 .tpc17-saved {
opacity: 0;
}
.tpc-17 :has(> #tpc17-fav:checked) .tpc17-saved {
opacity: 1;
}@import "tailwindcss";
@theme {
--color-tpc17-bg: #eef1f4;
--color-tpc17-bg: oklch(0.95 0.006 240);
--color-tpc17-card: #ffffff;
--color-tpc17-card: oklch(1 0 0);
--color-tpc17-ink: #101828;
--color-tpc17-ink: oklch(0.23 0.03 260);
--color-tpc17-ink2: #344054;
--color-tpc17-ink2: oklch(0.4 0.03 260);
--color-tpc17-mut: #5b6678;
--color-tpc17-mut: oklch(0.5 0.025 260);
--color-tpc17-edge: #e4e8ee;
--color-tpc17-edge: oklch(0.93 0.008 250);
--color-tpc17-ph: #d7dfe8;
--color-tpc17-ph: oklch(0.89 0.015 240);
--color-tpc17-ph2: #c3cdda;
--color-tpc17-ph2: oklch(0.84 0.022 245);
--color-tpc17-heart: #9aa5b4;
--color-tpc17-heart: oklch(0.71 0.018 250);
--color-tpc17-fav: #c8324a;
--color-tpc17-fav: oklch(0.55 0.18 15);
--color-tpc17-acc: #16688f;
--color-tpc17-acc: oklch(0.47 0.11 225);
--color-tpc17-onacc: #ffffff;
--color-tpc17-onacc: oklch(1 0 0);
--font-tpc17-sans: 'Manrope', system-ui, sans-serif;
}
.tpc-17 {
width: 100%;
min-height: 100svh;
}
.tpc-17 ::selection {
background: var(--color-tpc17-acc);
color: #fff;
}
/* The 'Saved' pill sits BEFORE the checkbox in source order, and peer-* only looks
forward — so one :has() rule owns the pill's visibility outright. Declaring it here
rather than fighting an opacity-0 utility also sidesteps the layering question:
this rule is unlayered, so nothing in @layer utilities can override it. */
.tpc-17 .tpc17-saved {
opacity: 0;
}
.tpc-17 :has(> #tpc17-fav:checked) .tpc17-saved {
opacity: 1;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tpc17-bg: #eef1f4;
--color-tpc17-bg: oklch(0.95 0.006 240);
--color-tpc17-card: #ffffff;
--color-tpc17-card: oklch(1 0 0);
--color-tpc17-ink: #101828;
--color-tpc17-ink: oklch(0.23 0.03 260);
--color-tpc17-ink2: #344054;
--color-tpc17-ink2: oklch(0.4 0.03 260);
--color-tpc17-mut: #5b6678;
--color-tpc17-mut: oklch(0.5 0.025 260);
--color-tpc17-edge: #e4e8ee;
--color-tpc17-edge: oklch(0.93 0.008 250);
--color-tpc17-ph: #d7dfe8;
--color-tpc17-ph: oklch(0.89 0.015 240);
--color-tpc17-ph2: #c3cdda;
--color-tpc17-ph2: oklch(0.84 0.022 245);
--color-tpc17-heart: #9aa5b4;
--color-tpc17-heart: oklch(0.71 0.018 250);
--color-tpc17-fav: #c8324a;
--color-tpc17-fav: oklch(0.55 0.18 15);
--color-tpc17-acc: #16688f;
--color-tpc17-acc: oklch(0.47 0.11 225);
--color-tpc17-onacc: #ffffff;
--color-tpc17-onacc: oklch(1 0 0);
--font-tpc17-sans: 'Manrope', system-ui, sans-serif;
}
.tpc-17 {
width: 100%;
min-height: 100svh;
}
.tpc-17 ::selection {
background: var(--color-tpc17-acc);
color: #fff;
}
/* The 'Saved' pill sits BEFORE the checkbox in source order, and peer-* only looks
forward — so one :has() rule owns the pill's visibility outright. Declaring it here
rather than fighting an opacity-0 utility also sidesteps the layering question:
this rule is unlayered, so nothing in @layer utilities can override it. */
.tpc-17 .tpc17-saved {
opacity: 0;
}
.tpc-17 :has(> #tpc17-fav:checked) .tpc17-saved {
opacity: 1;
}@import "tailwindcss";
@theme {
--color-tpc17-bg: #eef1f4;
--color-tpc17-bg: oklch(0.95 0.006 240);
--color-tpc17-card: #ffffff;
--color-tpc17-card: oklch(1 0 0);
--color-tpc17-ink: #101828;
--color-tpc17-ink: oklch(0.23 0.03 260);
--color-tpc17-ink2: #344054;
--color-tpc17-ink2: oklch(0.4 0.03 260);
--color-tpc17-mut: #5b6678;
--color-tpc17-mut: oklch(0.5 0.025 260);
--color-tpc17-edge: #e4e8ee;
--color-tpc17-edge: oklch(0.93 0.008 250);
--color-tpc17-ph: #d7dfe8;
--color-tpc17-ph: oklch(0.89 0.015 240);
--color-tpc17-ph2: #c3cdda;
--color-tpc17-ph2: oklch(0.84 0.022 245);
--color-tpc17-heart: #9aa5b4;
--color-tpc17-heart: oklch(0.71 0.018 250);
--color-tpc17-fav: #c8324a;
--color-tpc17-fav: oklch(0.55 0.18 15);
--color-tpc17-acc: #16688f;
--color-tpc17-acc: oklch(0.47 0.11 225);
--color-tpc17-onacc: #ffffff;
--color-tpc17-onacc: oklch(1 0 0);
--font-tpc17-sans: 'Manrope', system-ui, sans-serif;
}
.tpc-17 {
width: 100%;
min-height: 100svh;
}
.tpc-17 ::selection {
background: var(--color-tpc17-acc);
color: #fff;
}
/* The 'Saved' pill sits BEFORE the checkbox in source order, and peer-* only looks
forward — so one :has() rule owns the pill's visibility outright. Declaring it here
rather than fighting an opacity-0 utility also sidesteps the layering question:
this rule is unlayered, so nothing in @layer utilities can override it. */
.tpc-17 .tpc17-saved {
opacity: 0;
}
.tpc-17 :has(> #tpc17-fav:checked) .tpc17-saved {
opacity: 1;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Four
:checkedselectors from the CSS original become fourpeer-checked:/peer-focus-visible:variants on the elements they affect. The heart's resting colour, hover colour, checked colour and checked scale now all read in one class list. - Two reach problems, two answers. The heart's state variants must sit on the label, because that is the input's sibling — so the glyph is the label's own text and
peer-checked:text-tpc17-favlands directly on what changes. The 'Saved' pill is a previous sibling of the checkbox, which no forward-lookingpeer-variant can reach at all, so one unlayered:has()rule owns it outright. Both failures are silent ones: the CSS compiles, the selector just never matches. bg-tpc17-card/92— v4's slash opacity on a theme colour, replacing the original'srgba(255,255,255,.92). The token stays the source of truth.- Enhancement over the CSS original: the favourite control grew from 40px to
size-11(44px), gained a text 'Saved' pill so the state is not colour-only, and the pop transform is cancelled rather than slowed undermotion-reduce.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Real Estate Listing Card with Favourite Toggle
Source: https://codefronts.com/components/tailwind-css-product-cards/tailwind-real-estate-listing-card-favourite-toggle/
A property card led by price, with a status pill, a beds/baths/sqft feature row and a favourite heart that is a real checkbox — no script. peer-checked: fills and pops the heart, and because the same checkbox is read by a peer-checked:opacity-100 pill, the saved state is text as well as colour.
## HTML
```html
<section class="tpc-17 w-full min-h-svh grid place-items-center bg-tpc17-bg font-tpc17-sans px-5 py-12" aria-label="Real estate listing card demo">
<article class="w-[min(350px,100%)] rounded-[18px] overflow-hidden bg-tpc17-card shadow-[0_16px_40px_-22px_oklch(0.25_0.02_240/0.4)]">
<div class="relative aspect-[16/10] bg-linear-[135deg,var(--color-tpc17-ph),var(--color-tpc17-ph2)] bg-[url('https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=800&h=500&fit=crop&q=70')] bg-cover bg-center">
<div class="absolute top-3 left-3 flex items-center gap-2">
<span class="px-3 py-1.25 rounded-full bg-tpc17-acc text-tpc17-onacc text-[11px] font-bold tracking-[0.04em]">For Sale</span>
<span aria-hidden="true" class="tpc17-saved px-3 py-1.25 rounded-full bg-tpc17-card text-tpc17-fav text-[11px] font-bold tracking-[0.04em] transition-opacity duration-200 motion-reduce:transition-none">Saved</span>
</div>
<input type="checkbox" id="tpc17-fav" class="peer sr-only">
<label for="tpc17-fav" aria-label="Save to favourites" class="absolute top-3 right-3 grid place-items-center size-11 rounded-full cursor-pointer bg-tpc17-card/92 text-[20px] leading-none text-tpc17-heart shadow-[0_4px_12px_-4px_oklch(0.2_0.02_240/0.35)] transition-[color,scale] duration-200 hover:text-tpc17-fav peer-checked:text-tpc17-fav peer-checked:scale-110 peer-focus-visible:outline-3 peer-focus-visible:outline-tpc17-acc peer-focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:peer-checked:scale-100"><span aria-hidden="true">♥</span></label>
</div>
<div class="px-5 pt-4.5 pb-5">
<p class="text-[24px] font-extrabold text-tpc17-ink tabular-nums">$685,000</p>
<h3 class="mt-1 text-[16px] font-semibold text-tpc17-ink2">12 Larch Grove, Portland</h3>
<p class="mt-0.5 text-[13px] text-tpc17-mut">Detached · 2 storeys</p>
<ul class="mt-4 pt-4 border-t border-tpc17-edge flex justify-between list-none">
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">3</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Beds</span></li>
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">2</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Baths</span></li>
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">1,840</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Sq Ft</span></li>
</ul>
</div>
</article>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tpc17-bg: #eef1f4;
--color-tpc17-bg: oklch(0.95 0.006 240);
--color-tpc17-card: #ffffff;
--color-tpc17-card: oklch(1 0 0);
--color-tpc17-ink: #101828;
--color-tpc17-ink: oklch(0.23 0.03 260);
--color-tpc17-ink2: #344054;
--color-tpc17-ink2: oklch(0.4 0.03 260);
--color-tpc17-mut: #5b6678;
--color-tpc17-mut: oklch(0.5 0.025 260);
--color-tpc17-edge: #e4e8ee;
--color-tpc17-edge: oklch(0.93 0.008 250);
--color-tpc17-ph: #d7dfe8;
--color-tpc17-ph: oklch(0.89 0.015 240);
--color-tpc17-ph2: #c3cdda;
--color-tpc17-ph2: oklch(0.84 0.022 245);
--color-tpc17-heart: #9aa5b4;
--color-tpc17-heart: oklch(0.71 0.018 250);
--color-tpc17-fav: #c8324a;
--color-tpc17-fav: oklch(0.55 0.18 15);
--color-tpc17-acc: #16688f;
--color-tpc17-acc: oklch(0.47 0.11 225);
--color-tpc17-onacc: #ffffff;
--color-tpc17-onacc: oklch(1 0 0);
--font-tpc17-sans: 'Manrope', system-ui, sans-serif;
}
.tpc-17 {
width: 100%;
min-height: 100svh;
}
.tpc-17 ::selection {
background: var(--color-tpc17-acc);
color: #fff;
}
/* The 'Saved' pill sits BEFORE the checkbox in source order, and peer-* only looks
forward — so one :has() rule owns the pill's visibility outright. Declaring it here
rather than fighting an opacity-0 utility also sidesteps the layering question:
this rule is unlayered, so nothing in @layer utilities can override it. */
.tpc-17 .tpc17-saved {
opacity: 0;
}
.tpc-17 :has(> #tpc17-fav:checked) .tpc17-saved {
opacity: 1;
}
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Real Estate Listing Card with Favourite Toggle
Source: https://codefronts.com/components/tailwind-css-product-cards/tailwind-real-estate-listing-card-favourite-toggle/
A property card led by price, with a status pill, a beds/baths/sqft feature row and a favourite heart that is a real checkbox — no script. peer-checked: fills and pops the heart, and because the same checkbox is read by a peer-checked:opacity-100 pill, the saved state is text as well as colour.
## HTML
```html
<section class="tpc-17 w-full min-h-svh grid place-items-center bg-tpc17-bg font-tpc17-sans px-5 py-12" aria-label="Real estate listing card demo">
<article class="w-[min(350px,100%)] rounded-[18px] overflow-hidden bg-tpc17-card shadow-[0_16px_40px_-22px_oklch(0.25_0.02_240/0.4)]">
<div class="relative aspect-[16/10] bg-linear-[135deg,var(--color-tpc17-ph),var(--color-tpc17-ph2)] bg-[url('https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=800&h=500&fit=crop&q=70')] bg-cover bg-center">
<div class="absolute top-3 left-3 flex items-center gap-2">
<span class="px-3 py-1.25 rounded-full bg-tpc17-acc text-tpc17-onacc text-[11px] font-bold tracking-[0.04em]">For Sale</span>
<span aria-hidden="true" class="tpc17-saved px-3 py-1.25 rounded-full bg-tpc17-card text-tpc17-fav text-[11px] font-bold tracking-[0.04em] transition-opacity duration-200 motion-reduce:transition-none">Saved</span>
</div>
<input type="checkbox" id="tpc17-fav" class="peer sr-only">
<label for="tpc17-fav" aria-label="Save to favourites" class="absolute top-3 right-3 grid place-items-center size-11 rounded-full cursor-pointer bg-tpc17-card/92 text-[20px] leading-none text-tpc17-heart shadow-[0_4px_12px_-4px_oklch(0.2_0.02_240/0.35)] transition-[color,scale] duration-200 hover:text-tpc17-fav peer-checked:text-tpc17-fav peer-checked:scale-110 peer-focus-visible:outline-3 peer-focus-visible:outline-tpc17-acc peer-focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:peer-checked:scale-100"><span aria-hidden="true">♥</span></label>
</div>
<div class="px-5 pt-4.5 pb-5">
<p class="text-[24px] font-extrabold text-tpc17-ink tabular-nums">$685,000</p>
<h3 class="mt-1 text-[16px] font-semibold text-tpc17-ink2">12 Larch Grove, Portland</h3>
<p class="mt-0.5 text-[13px] text-tpc17-mut">Detached · 2 storeys</p>
<ul class="mt-4 pt-4 border-t border-tpc17-edge flex justify-between list-none">
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">3</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Beds</span></li>
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">2</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Baths</span></li>
<li class="flex flex-col items-center gap-0.5"><b class="text-[17px] font-bold text-tpc17-ink tabular-nums">1,840</b><span class="text-[11px] tracking-[0.06em] uppercase text-tpc17-mut">Sq Ft</span></li>
</ul>
</div>
</article>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tpc17-bg: #eef1f4;
--color-tpc17-bg: oklch(0.95 0.006 240);
--color-tpc17-card: #ffffff;
--color-tpc17-card: oklch(1 0 0);
--color-tpc17-ink: #101828;
--color-tpc17-ink: oklch(0.23 0.03 260);
--color-tpc17-ink2: #344054;
--color-tpc17-ink2: oklch(0.4 0.03 260);
--color-tpc17-mut: #5b6678;
--color-tpc17-mut: oklch(0.5 0.025 260);
--color-tpc17-edge: #e4e8ee;
--color-tpc17-edge: oklch(0.93 0.008 250);
--color-tpc17-ph: #d7dfe8;
--color-tpc17-ph: oklch(0.89 0.015 240);
--color-tpc17-ph2: #c3cdda;
--color-tpc17-ph2: oklch(0.84 0.022 245);
--color-tpc17-heart: #9aa5b4;
--color-tpc17-heart: oklch(0.71 0.018 250);
--color-tpc17-fav: #c8324a;
--color-tpc17-fav: oklch(0.55 0.18 15);
--color-tpc17-acc: #16688f;
--color-tpc17-acc: oklch(0.47 0.11 225);
--color-tpc17-onacc: #ffffff;
--color-tpc17-onacc: oklch(1 0 0);
--font-tpc17-sans: 'Manrope', system-ui, sans-serif;
}
.tpc-17 {
width: 100%;
min-height: 100svh;
}
.tpc-17 ::selection {
background: var(--color-tpc17-acc);
color: #fff;
}
/* The 'Saved' pill sits BEFORE the checkbox in source order, and peer-* only looks
forward — so one :has() rule owns the pill's visibility outright. Declaring it here
rather than fighting an opacity-0 utility also sidesteps the layering question:
this rule is unlayered, so nothing in @layer utilities can override it. */
.tpc-17 .tpc17-saved {
opacity: 0;
}
.tpc-17 :has(> #tpc17-fav:checked) .tpc17-saved {
opacity: 1;
}
```How this works
A favourite is a checkbox.
<input type="checkbox" id="tpc17-fav" class="peer sr-only">
<label for="tpc17-fav" class="text-tpc17-heart
peer-checked:text-tpc17-fav peer-checked:scale-110
peer-focus-visible:outline-3">♥</label>Note where the variants sit. peer-* compiles to a sibling combinator, so they only work on the label — the input's actual sibling. The glyph is therefore the label's own text rather than a nested span: put peer-checked: on a child and it silently never matches, which is the easiest way to ship a dead toggle.
Checked state, keyboard operation, the announced 'checked'/'not checked' and form submission all arrive with the input. The CSS version of this card wrote the same thing with :checked + selectors in a stylesheet; the port moves those declarations onto the two elements that change, which is the only difference that matters.
The heart is not the only signal. A 'Saved' pill sits beside the status pill at opacity-0 and comes up on group-has-[#tpc17-fav:checked]/fav:opacity-100 — the pill sits before the checkbox in source order, so no forward-looking peer- variant can reach it, and the media wrapper becomes a named group instead. A colour change on a 20px glyph is a weak state indicator; a word is not.
Price leads, features close. The hierarchy is price → address → type → features, with the feature row set off by a hairline. Every number in it is paired with a visible label, because a bare '3 / 2 / 1,840' row is meaningless to anyone who cannot see the icons a designer would reach for.
Make it yours
- Status pill colour per state: recolour
--color-tpc17-acc, or add a second token for 'Under Offer'. - Units: swap Sq Ft for m², and add a fourth feature cell (parking, lot size) — the row is
justify-betweenand absorbs it. - Bookmark instead of a heart: change the glyph and the
aria-label; nothing else moves. - Make the media a link to the listing, keeping the heart as a separate control above it.
- Drop the 'Saved' pill if space is tight — but then strengthen the heart's checked state beyond colour.
Gotchas — read before shipping
sr-only, neverhidden. Adisplay:nonecheckbox is unfocusable and the heart becomes mouse-only.- Keep the heart glyph as the label's own text. Nesting it in a
<span>and movingpeer-checked:onto that span compiles fine and never matches —peer-reaches siblings, not children. - The label needs an
aria-label('Save to favourites'); the glyph inside it isaria-hiddendecoration. - Feature numbers need their text labels. Icon-only rows fail AT and lose the SEO value of the listing's specs.
peerreaches siblings only, so state variants belong on the label (with[&>span]to reach the glyph), never on the glyph itself — and the input must precede the label in source order even though it is invisible.- Keep the price the strongest element. A card that shouts its address and whispers its price inverts the decision the shopper is making.
- The scale 'pop' is a transform, so it is cancelled under
motion-reducerather than slowed — the colour change alone still carries the state.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
A :checked-driven label toggle plus transforms are universally supported; the floor quoted is Tailwind v4's own generated sheet. The favourite works with no JavaScript.