Tailwind Card: 16 Examples for Blogs, Products and Pricing

Tailwind has no card class. Every Tailwind card is put together by hand from background, padding, radius, border and shadow utilities, so the layout and hover states travel with the markup wherever it gets pasted.

Below are 16 Tailwind card examples you can copy, followed by a guide to layouts, clickable cards, grids, dark mode and the v4 changes that break old snippets.

By Bogdan Sandu · Updated October 29, 2026

Every example is built with Tailwind CSS v4.3 utility classes. To try one quickly, add the Play CDN tags below to your page and paste the example's HTML, CSS and JS. The dark variant line makes dark: classes follow a .dark class instead of the system setting. For production, install Tailwind with Vite or the CLI.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>

Blog, media and content cards

Article cards with photos, a horizontal card that stacks on phones, an image card with a translucent overlay, an event card with a date block and testimonial cards, all composed from surface, spacing and type utilities.

Blog post cards with image zoom

  • overflow-hidden
  • aspect-[3/2] object-cover
  • motion-safe:group-hover:scale-105
  • line-clamp-3
  • mt-auto

Each card is an article with overflow-hidden and rounded-2xl, so the photo follows the corners and the zoom from motion-safe:group-hover:scale-105 stays inside the frame. flex flex-col on the card and mt-auto on the author row keep both footers on one line even when the excerpts differ, and line-clamp-3 caps the excerpt.

View code
<div class="grid w-full max-w-3xl gap-6 sm:grid-cols-2">
  <article class="group flex flex-col overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-stone-900/5">
    <div class="relative overflow-hidden">
      <img src="fjord.webp" width="800" height="600" alt="Deep blue fjord between granite cliffs, seen from a rocky ledge" class="aspect-[3/2] w-full object-cover motion-safe:transition-transform motion-safe:duration-500 motion-safe:group-hover:scale-105">
      <span class="absolute top-3 left-3 rounded-full bg-white/90 px-2.5 py-1 text-xs font-semibold text-emerald-800">Travel</span>
    </div>
    <div class="flex flex-1 flex-col p-5">
      <h3 class="text-lg/snug font-semibold text-stone-900"><a href="#" class="rounded-sm hover:text-emerald-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700">Twelve days along the western fjords by ferry and on foot</a></h3>
      <p class="mt-2 line-clamp-3 text-sm/6 text-stone-600">Local ferries link most of the villages, so you can skip the rental car, carry one pack and still reach the trailheads that the tour buses miss.</p>
      <div class="mt-auto flex items-center gap-3 pt-5">
        <svg class="size-9 shrink-0" viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="20" r="20" fill="#d1fae5"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" fill="#065f46" font-family="system-ui, sans-serif">LV</text></svg>
        <div class="text-sm">
          <p class="font-semibold text-stone-900">Lena Voss</p>
          <p class="text-stone-600"><time datetime="2026-10-12">Oct 12, 2026</time> · 8 min read</p>
        </div>
      </div>
    </div>
  </article>
  <article class="group flex flex-col overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-stone-900/5">
    <div class="relative overflow-hidden">
      <img src="red-rock-canyon.webp" width="800" height="600" alt="Red sandstone rocks glowing at sunset under a cloudy sky" class="aspect-[3/2] w-full object-cover motion-safe:transition-transform motion-safe:duration-500 motion-safe:group-hover:scale-105">
      <span class="absolute top-3 left-3 rounded-full bg-white/90 px-2.5 py-1 text-xs font-semibold text-orange-800">Photography</span>
    </div>
    <div class="flex flex-1 flex-col p-5">
      <h3 class="text-lg/snug font-semibold text-stone-900"><a href="#" class="rounded-sm hover:text-orange-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-700">Field notes on desert light</a></h3>
      <p class="mt-2 line-clamp-3 text-sm/6 text-stone-600">Sandstone turns red for about twenty minutes before sunset. Here is how to plan the walk in, meter for the highlights and keep the shadows from going muddy.</p>
      <div class="mt-auto flex items-center gap-3 pt-5">
        <svg class="size-9 shrink-0" viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="20" r="20" fill="#ffedd5"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" fill="#9a3412" font-family="system-ui, sans-serif">DO</text></svg>
        <div class="text-sm">
          <p class="font-semibold text-stone-900">Daniel Okafor</p>
          <p class="text-stone-600"><time datetime="2026-09-28">Sep 28, 2026</time> · 5 min read</p>
        </div>
      </div>
    </div>
  </article>
</div>

Horizontal card that stacks on phones

  • flex-col md:flex-row
  • h-56 md:h-auto
  • md:w-2/5
  • min-w-0

The card is a flex column on phones and a row from the md breakpoint. The image gets an explicit h-56 while stacked and md:h-auto md:w-2/5 beside the text, so object-cover always has a box to crop against. min-w-0 on the body lets the title wrap instead of pushing the card wider.

View code
<article class="flex w-full max-w-3xl flex-col overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-teal-950/10 md:flex-row">
  <img src="coast-road.webp" width="800" height="600" alt="Pine-covered cliffs above a calm sea, with a narrow road winding along the coastline" class="h-56 w-full object-cover md:h-auto md:w-2/5">
  <div class="flex min-w-0 flex-1 flex-col p-6">
    <div class="flex items-start justify-between gap-4">
      <div class="min-w-0">
        <h3 class="text-xl font-semibold text-slate-900">Cliffside cabin above Cala Verde</h3>
        <p class="mt-1 text-sm text-slate-600">Entire cabin · 2 guests · 1 bedroom</p>
      </div>
      <p class="flex shrink-0 items-center gap-1 text-sm font-semibold text-slate-900"><svg class="size-4 text-amber-500" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3l2.7 5.6 6.1.8-4.5 4.2 1.1 6.1L12 16.8l-5.4 2.9 1.1-6.1-4.5-4.2 6.1-.8z"/></svg>4.92<span class="sr-only"> out of 5, </span><span class="font-normal text-slate-600">(38)</span></p>
    </div>
    <ul role="list" class="mt-4 flex flex-wrap gap-2 text-xs font-medium text-teal-900">
      <li class="rounded-full bg-teal-50 px-2.5 py-1 ring-1 ring-teal-700/15 ring-inset">Sea view</li>
      <li class="rounded-full bg-teal-50 px-2.5 py-1 ring-1 ring-teal-700/15 ring-inset">Wood stove</li>
      <li class="rounded-full bg-teal-50 px-2.5 py-1 ring-1 ring-teal-700/15 ring-inset">Trail access</li>
    </ul>
    <p class="mt-4 text-sm/6 text-slate-600">A stone cabin on the coast road, ten minutes on foot from a small pebble beach. Morning light comes straight through the kitchen window.</p>
    <div class="mt-auto flex flex-wrap items-center justify-between gap-4 pt-6">
      <p class="text-slate-900"><span class="text-2xl font-semibold">$214</span> <span class="text-sm text-slate-600">per night</span></p>
      <button type="button" class="min-h-11 cursor-pointer rounded-lg bg-teal-700 px-5 text-sm font-semibold text-white hover:bg-teal-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">Check availability</button>
    </div>
  </div>
</article>

Image card with a translucent overlay

  • absolute inset-0
  • bg-slate-950/55
  • group-hover:bg-slate-950/70
  • after:absolute after:inset-0

The photo fills the card with absolute inset-0 and object-cover, and a second layer with bg-slate-950/55 darkens it evenly so white text keeps its contrast without a gradient. Hovering or focusing the card deepens the overlay through group-hover: and group-has-[a:focus-visible]:, and the title link is stretched over the whole card with after:absolute after:inset-0.

View code
<article class="group relative isolate w-full max-w-xl overflow-hidden rounded-3xl bg-slate-900 has-[a:focus-visible]:outline-2 has-[a:focus-visible]:outline-offset-4 has-[a:focus-visible]:outline-sky-600">
  <img src="city-skyline.webp" width="800" height="600" alt="" class="absolute inset-0 -z-10 size-full object-cover motion-safe:transition-transform motion-safe:duration-700 motion-safe:group-hover:scale-105">
  <div class="absolute inset-0 -z-10 bg-slate-950/55 motion-safe:transition-colors group-hover:bg-slate-950/70 group-has-[a:focus-visible]:bg-slate-950/70"></div>
  <div class="flex min-h-80 flex-col justify-end p-6 sm:min-h-96 sm:p-8">
    <h3 class="max-w-sm text-2xl/tight font-semibold text-white sm:text-3xl/tight"><a href="#" class="after:absolute after:inset-0 focus-visible:outline-hidden">48 hours in the lakefront city</a></h3>
    <p class="mt-3 max-w-md text-sm/6 text-slate-100">Rooftop views at sunrise, the river walk, three food halls and the only architecture boat tour worth the ticket.</p>
    <ul role="list" class="mt-5 flex flex-wrap gap-x-5 gap-y-2 text-sm font-medium text-white">
      <li class="flex items-center gap-1.5"><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/></svg>14 places</li>
      <li class="flex items-center gap-1.5"><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></svg>City guide, 2 days</li>
    </ul>
  </div>
</article>

Event card with a date block and RSVP

  • <time datetime>
  • aria-pressed:
  • -space-x-2
  • tabular-nums

Click RSVP.

The date block is a time element with a machine-readable datetime, styled as a tinted tile with flex-col and tabular-nums. The RSVP button is a toggle: its aria-pressed attribute drives the aria-pressed:bg-amber-50 and aria-pressed:ring-1 styles, and the script only flips the attribute, the label and the attendee count. -space-x-2 overlaps the SVG initials avatars.

View code
<article class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-sm ring-1 ring-stone-900/5 sm:p-6">
  <div class="flex gap-4 sm:gap-5">
    <time datetime="2026-11-14T18:30" class="flex w-16 shrink-0 flex-col items-center self-start rounded-xl bg-amber-100 py-2.5 text-amber-950 tabular-nums">
      <span class="text-xs font-semibold uppercase">Nov</span>
      <span class="text-3xl/none font-bold">14</span>
      <span class="mt-1 text-xs font-medium">Sat</span>
    </time>
    <div class="min-w-0">
      <h3 class="text-lg/snug font-semibold text-stone-900">Night market photo walk</h3>
      <ul role="list" class="mt-2 space-y-1.5 text-sm text-stone-600">
        <li class="flex items-center gap-2"><svg class="size-4 shrink-0 text-stone-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></svg>6:30 PM to 9:00 PM</li>
        <li class="flex items-center gap-2"><svg class="size-4 shrink-0 text-stone-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/></svg>Pier 7, Harbor District</li>
      </ul>
    </div>
  </div>
  <p class="mt-4 text-sm/6 text-stone-600">Bring any camera. We walk the stalls for two hours, then review a few frames over tea. Tripods are not allowed on the pier.</p>
  <div class="mt-5 flex flex-wrap items-center justify-between gap-4 border-t border-stone-200 pt-4">
    <div class="flex items-center gap-3">
      <div class="flex -space-x-2" aria-hidden="true">
        <svg class="size-8 rounded-full ring-2 ring-white" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="#fde68a"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" fill="#78350f" font-family="system-ui, sans-serif">AK</text></svg>
        <svg class="size-8 rounded-full ring-2 ring-white" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="#bae6fd"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" fill="#0c4a6e" font-family="system-ui, sans-serif">JM</text></svg>
        <svg class="size-8 rounded-full ring-2 ring-white" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="#fecdd3"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" fill="#881337" font-family="system-ui, sans-serif">SR</text></svg>
      </div>
      <p class="text-sm text-stone-600"><span id="tcd-going" class="font-semibold text-stone-900 tabular-nums">24</span> going</p>
    </div>
    <button type="button" id="tcd-rsvp" aria-pressed="false" class="inline-flex min-h-11 cursor-pointer items-center gap-2 rounded-lg bg-amber-500 px-5 text-sm font-semibold text-amber-950 hover:bg-amber-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 aria-pressed:bg-amber-50 aria-pressed:ring-1 aria-pressed:ring-amber-600/40 aria-pressed:ring-inset aria-pressed:hover:bg-amber-100"><svg class="hidden size-4 in-aria-pressed:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg><span id="tcd-rsvp-label">RSVP</span></button>
  </div>
</article>

Testimonial cards on a dark surface

  • figure / blockquote
  • ring-white/10
  • md:grid-cols-[3fr_2fr]
  • sr-only

The quote card is a figure with a blockquote and a figcaption, so the attribution is tied to the quote for screen readers. On the dark surface the cards drop shadows and use bg-slate-900 with ring-1 ring-white/10 as the edge, and the star row carries an sr-only rating. md:grid-cols-[3fr_2fr] gives the quote the wider column.

View code
<div class="grid w-full max-w-3xl gap-5 md:grid-cols-[3fr_2fr]">
  <figure class="flex flex-col rounded-2xl bg-slate-900 p-6 ring-1 ring-white/10 sm:p-8">
    <div class="flex gap-1 text-amber-400" role="img" aria-label="Rated 5 out of 5">
      <svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3l2.7 5.6 6.1.8-4.5 4.2 1.1 6.1L12 16.8l-5.4 2.9 1.1-6.1-4.5-4.2 6.1-.8z"/></svg>
      <svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3l2.7 5.6 6.1.8-4.5 4.2 1.1 6.1L12 16.8l-5.4 2.9 1.1-6.1-4.5-4.2 6.1-.8z"/></svg>
      <svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3l2.7 5.6 6.1.8-4.5 4.2 1.1 6.1L12 16.8l-5.4 2.9 1.1-6.1-4.5-4.2 6.1-.8z"/></svg>
      <svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3l2.7 5.6 6.1.8-4.5 4.2 1.1 6.1L12 16.8l-5.4 2.9 1.1-6.1-4.5-4.2 6.1-.8z"/></svg>
      <svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3l2.7 5.6 6.1.8-4.5 4.2 1.1 6.1L12 16.8l-5.4 2.9 1.1-6.1-4.5-4.2 6.1-.8z"/></svg>
    </div>
    <blockquote class="mt-5 text-lg/8 text-slate-100">
      <p>We moved forty landing pages to the new card system in a week. Designers stopped filing spacing bugs, and our product cards finally look the same in the shop and in the newsletter.</p>
    </blockquote>
    <figcaption class="mt-auto flex items-center gap-3 pt-6">
      <svg class="size-11 shrink-0" viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="20" r="20" fill="#334155"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" fill="#e2e8f0" font-family="system-ui, sans-serif">MK</text></svg>
      <div class="text-sm">
        <p class="font-semibold text-white">Marta Kowalczyk</p>
        <p class="text-slate-400">Head of design, Fernwood Studio</p>
      </div>
    </figcaption>
  </figure>
  <div class="flex flex-col rounded-2xl bg-amber-400 p-6 text-amber-950 sm:p-8">
    <p class="text-5xl font-bold tracking-tight tabular-nums">4.9</p>
    <p class="mt-1 text-sm font-semibold">Average rating from 1,312 reviews</p>
    <dl class="mt-6 space-y-3 text-sm">
      <div>
        <div class="flex justify-between"><dt>5 stars</dt><dd class="font-semibold tabular-nums">91%</dd></div>
        <div class="mt-1 h-2 rounded-full bg-amber-950/15"><div class="h-2 w-[91%] rounded-full bg-amber-950"></div></div>
      </div>
      <div>
        <div class="flex justify-between"><dt>4 stars</dt><dd class="font-semibold tabular-nums">7%</dd></div>
        <div class="mt-1 h-2 rounded-full bg-amber-950/15"><div class="h-2 w-[7%] rounded-full bg-amber-950"></div></div>
      </div>
      <div>
        <div class="flex justify-between"><dt>3 stars or fewer</dt><dd class="font-semibold tabular-nums">2%</dd></div>
        <div class="mt-1 h-2 rounded-full bg-amber-950/15"><div class="h-2 w-[2%] rounded-full bg-amber-950"></div></div>
      </div>
    </dl>
  </div>
</div>

Product, pricing and stat cards

Cards that sell or report: a product card with color swatches and an add-to-cart state, three pricing tiers with a billing switch and a row of KPI cards with sparklines.

Product card with swatches and add to cart

  • has-checked:ring-2
  • has-focus-visible:outline-2
  • data-added:
  • line-through

Pick a color, switch the billing period or add the product to the cart.

The swatches are real radio buttons hidden with sr-only inside their labels, so has-checked:ring-2 draws the selected ring and has-focus-visible: shows keyboard focus with no script. The cart button uses a data-added attribute: data-added:bg-emerald-700 and the in-data-added: icon swap come from Tailwind, while the script sets the attribute, the color name and a polite live region message.

View code
<article class="w-full max-w-sm overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-neutral-900/5">
  <div class="relative">
    <img src="snow-camp.webp" width="800" height="600" alt="Yellow tents pitched on snow below a jagged mountain ridge" class="aspect-[4/3] w-full object-cover">
    <span class="absolute top-3 left-3 rounded-md bg-orange-600 px-2 py-1 text-xs font-bold text-white">Save $60</span>
  </div>
  <div class="p-5">
    <div class="flex items-start justify-between gap-3">
      <h3 class="text-lg font-semibold text-neutral-900">Ridgeline 2P tent</h3>
      <p class="shrink-0 text-right"><span class="block text-lg font-semibold text-neutral-900">$389</span><span class="block text-sm text-neutral-500 line-through"><span class="sr-only">Was </span>$449</span></p>
    </div>
    <p class="mt-1 text-sm text-neutral-600">Four-season, 2.1 kg, sets up in under five minutes.</p>
    <fieldset class="mt-4">
      <legend class="text-sm text-neutral-700">Color: <span id="tcd-color-name" class="font-semibold text-neutral-900">Saffron</span></legend>
      <div class="mt-2 flex gap-3">
        <label class="relative grid size-9 cursor-pointer place-items-center rounded-full ring-offset-2 has-checked:ring-2 has-checked:ring-neutral-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-4 has-focus-visible:outline-orange-600"><input type="radio" name="tcd-color" value="Saffron" class="tcd-swatch sr-only" checked><span class="size-7 rounded-full bg-amber-400 ring-1 ring-black/10 ring-inset"></span><span class="sr-only">Saffron</span></label>
        <label class="relative grid size-9 cursor-pointer place-items-center rounded-full ring-offset-2 has-checked:ring-2 has-checked:ring-neutral-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-4 has-focus-visible:outline-orange-600"><input type="radio" name="tcd-color" value="Glacier" class="tcd-swatch sr-only"><span class="size-7 rounded-full bg-sky-300 ring-1 ring-black/10 ring-inset"></span><span class="sr-only">Glacier</span></label>
        <label class="relative grid size-9 cursor-pointer place-items-center rounded-full ring-offset-2 has-checked:ring-2 has-checked:ring-neutral-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-4 has-focus-visible:outline-orange-600"><input type="radio" name="tcd-color" value="Moss" class="tcd-swatch sr-only"><span class="size-7 rounded-full bg-emerald-800 ring-1 ring-black/10 ring-inset"></span><span class="sr-only">Moss</span></label>
        <label class="relative grid size-9 cursor-pointer place-items-center rounded-full ring-offset-2 has-checked:ring-2 has-checked:ring-neutral-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-4 has-focus-visible:outline-orange-600"><input type="radio" name="tcd-color" value="Graphite" class="tcd-swatch sr-only"><span class="size-7 rounded-full bg-neutral-700 ring-1 ring-black/10 ring-inset"></span><span class="sr-only">Graphite</span></label>
      </div>
    </fieldset>
    <button type="button" id="tcd-add" class="mt-5 inline-flex min-h-12 w-full cursor-pointer items-center justify-center gap-2 rounded-xl bg-neutral-900 px-5 text-sm font-semibold text-white hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600 data-added:bg-emerald-700 data-added:hover:bg-emerald-800">
      <svg class="size-5 in-data-added:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4h2l2.2 10.2a1.5 1.5 0 0 0 1.5 1.2h8.6a1.5 1.5 0 0 0 1.5-1.1L21 8H6.2"/><circle cx="9.5" cy="19.5" r="1.3"/><circle cx="17" cy="19.5" r="1.3"/></svg>
      <svg class="hidden size-5 in-data-added:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>
      <span id="tcd-add-label">Add to cart</span>
    </button>
    <p id="tcd-cart-msg" class="sr-only" aria-live="polite"></p>
  </div>
</article>

Three pricing tiers with a billing switch

  • group-has-[#id:checked]:
  • has-checked:bg-violet-700
  • ring-2 ring-violet-600
  • md:grid-cols-3

Pick a color, switch the billing period or add the product to the cart.

The billing switch is two radio buttons, and the wrapper is a group, so group-has-[#tcd-yearly:checked]:hidden and its inline counterpart swap every price with no JavaScript. The highlighted tier gets ring-2 ring-violet-600 and a solid button, and each card is a flex column with mt-auto on the button so all three line up at the bottom.

View code
<div class="group w-full max-w-4xl">
  <fieldset class="mx-auto flex w-fit rounded-full bg-white p-1 shadow-xs ring-1 ring-slate-200">
    <legend class="sr-only">Billing period</legend>
    <label class="min-h-10 cursor-pointer content-center rounded-full px-4 text-sm font-semibold text-slate-700 has-checked:bg-violet-700 has-checked:text-white has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-violet-700"><input type="radio" name="tcd-billing" value="monthly" class="sr-only" checked>Monthly</label>
    <label class="min-h-10 cursor-pointer content-center rounded-full px-4 text-sm font-semibold text-slate-700 has-checked:bg-violet-700 has-checked:text-white has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-violet-700"><input type="radio" name="tcd-billing" value="yearly" id="tcd-yearly" class="sr-only">Yearly, 2 months free</label>
  </fieldset>
  <div class="mt-8 grid gap-5 md:grid-cols-3 md:items-start">
    <section class="flex h-full flex-col rounded-2xl bg-white p-6 ring-1 ring-slate-200">
      <h3 class="text-base font-semibold text-slate-900">Starter</h3>
      <p class="mt-1 text-sm text-slate-600">For a personal site or a first shop.</p>
      <p class="mt-5 text-slate-900"><span class="text-4xl font-semibold tracking-tight">$0</span> <span class="text-sm text-slate-600">forever</span></p>
      <ul role="list" class="mt-6 mb-8 space-y-2.5 text-sm text-slate-700">
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>1 project</li>
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>5 GB storage</li>
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>Community support</li>
      </ul>
      <a href="#" class="mt-auto block rounded-lg px-4 py-2.5 text-center text-sm font-semibold text-violet-800 ring-1 ring-violet-300 ring-inset hover:bg-violet-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">Start free</a>
    </section>
    <section class="relative flex h-full flex-col rounded-2xl bg-white p-6 shadow-lg ring-2 ring-violet-600">
      <div class="flex items-center justify-between gap-3">
        <h3 class="text-base font-semibold text-violet-800">Studio</h3>
        <span class="rounded-full bg-violet-100 px-2.5 py-1 text-xs font-semibold text-violet-800">Most popular</span>
      </div>
      <p class="mt-1 text-sm text-slate-600">For freelancers with client work.</p>
      <p class="mt-5 text-slate-900"><span class="text-4xl font-semibold tracking-tight group-has-[#tcd-yearly:checked]:hidden">$24</span><span class="hidden text-4xl font-semibold tracking-tight group-has-[#tcd-yearly:checked]:inline">$20</span> <span class="text-sm text-slate-600">per month<span class="hidden group-has-[#tcd-yearly:checked]:inline">, billed yearly</span></span></p>
      <ul role="list" class="mt-6 mb-8 space-y-2.5 text-sm text-slate-700">
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>15 projects</li>
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>100 GB storage</li>
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>Custom domains</li>
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>Client review links</li>
      </ul>
      <a href="#" class="mt-auto block rounded-lg bg-violet-700 px-4 py-2.5 text-center text-sm font-semibold text-white shadow-xs hover:bg-violet-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">Choose Studio</a>
    </section>
    <section class="flex h-full flex-col rounded-2xl bg-white p-6 ring-1 ring-slate-200">
      <h3 class="text-base font-semibold text-slate-900">Agency</h3>
      <p class="mt-1 text-sm text-slate-600">For teams running many client sites.</p>
      <p class="mt-5 text-slate-900"><span class="text-4xl font-semibold tracking-tight group-has-[#tcd-yearly:checked]:hidden">$59</span><span class="hidden text-4xl font-semibold tracking-tight group-has-[#tcd-yearly:checked]:inline">$49</span> <span class="text-sm text-slate-600">per month<span class="hidden group-has-[#tcd-yearly:checked]:inline">, billed yearly</span></span></p>
      <ul role="list" class="mt-6 mb-8 space-y-2.5 text-sm text-slate-700">
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>Unlimited projects</li>
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>1 TB storage</li>
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>10 team seats</li>
        <li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-violet-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>Priority support</li>
      </ul>
      <a href="#" class="mt-auto block rounded-lg px-4 py-2.5 text-center text-sm font-semibold text-violet-800 ring-1 ring-violet-300 ring-inset hover:bg-violet-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">Talk to sales</a>
    </section>
  </div>
</div>

KPI stat cards with trend badges

  • dl / dt / dd
  • sm:grid-cols-2 md:grid-cols-4
  • tabular-nums
  • vector-effect

Pick a color, switch the billing period or add the product to the cart.

The row is one description list: each card is a div holding a dt label and dd values, which keeps label and number paired for assistive tech. The trend badges pair color with an arrow and sr-only text, so the direction does not rely on red and green alone, and the sparkline SVG uses currentColor, so text-emerald-600 or text-rose-600 colors the line.

View code
<dl class="grid w-full max-w-5xl gap-4 sm:grid-cols-2 md:grid-cols-4">
  <div class="rounded-2xl bg-white p-5 shadow-xs ring-1 ring-slate-900/5">
    <dt class="text-sm font-medium text-slate-600">Revenue</dt>
    <dd class="mt-2 flex flex-wrap items-baseline gap-x-2 gap-y-1"><span class="text-2xl font-semibold tracking-tight text-slate-900 tabular-nums">$48,210</span><span class="inline-flex items-center gap-0.5 rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-700"><svg class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5m0 0l-6 6m6-6l6 6"/></svg><span class="sr-only">Up</span>12.4%</span></dd>
    <dd class="mt-4"><svg class="h-10 w-full text-emerald-600" viewBox="0 0 120 40" preserveAspectRatio="none" aria-hidden="true"><polyline fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" vector-effect="non-scaling-stroke" points="0,32 15,29 30,31 45,24 60,25 75,18 90,20 105,11 120,6"/></svg></dd>
  </div>
  <div class="rounded-2xl bg-white p-5 shadow-xs ring-1 ring-slate-900/5">
    <dt class="text-sm font-medium text-slate-600">Orders</dt>
    <dd class="mt-2 flex flex-wrap items-baseline gap-x-2 gap-y-1"><span class="text-2xl font-semibold tracking-tight text-slate-900 tabular-nums">1,284</span><span class="inline-flex items-center gap-0.5 rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-700"><svg class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5m0 0l-6 6m6-6l6 6"/></svg><span class="sr-only">Up</span>4.1%</span></dd>
    <dd class="mt-4"><svg class="h-10 w-full text-emerald-600" viewBox="0 0 120 40" preserveAspectRatio="none" aria-hidden="true"><polyline fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" vector-effect="non-scaling-stroke" points="0,26 15,28 30,22 45,24 60,19 75,22 90,17 105,18 120,14"/></svg></dd>
  </div>
  <div class="rounded-2xl bg-white p-5 shadow-xs ring-1 ring-slate-900/5">
    <dt class="text-sm font-medium text-slate-600">Refund rate</dt>
    <dd class="mt-2 flex flex-wrap items-baseline gap-x-2 gap-y-1"><span class="text-2xl font-semibold tracking-tight text-slate-900 tabular-nums">2.8%</span><span class="inline-flex items-center gap-0.5 rounded-full bg-rose-50 px-2 py-0.5 text-xs font-semibold text-rose-700"><svg class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14m0 0l-6-6m6 6l6-6"/></svg><span class="sr-only">Worse by</span>0.6 pts</span></dd>
    <dd class="mt-4"><svg class="h-10 w-full text-rose-600" viewBox="0 0 120 40" preserveAspectRatio="none" aria-hidden="true"><polyline fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" vector-effect="non-scaling-stroke" points="0,30 15,31 30,27 45,28 60,22 75,24 90,18 105,16 120,12"/></svg></dd>
  </div>
  <div class="rounded-2xl bg-sky-900 p-5 text-white shadow-xs">
    <dt class="text-sm font-medium text-sky-100">Monthly goal</dt>
    <dd class="mt-2 text-2xl font-semibold tracking-tight tabular-nums">78%</dd>
    <dd class="mt-4">
      <div class="h-2.5 rounded-full bg-white/20" role="progressbar" aria-valuenow="78" aria-valuemin="0" aria-valuemax="100" aria-label="Monthly revenue goal"><div class="h-2.5 w-[78%] rounded-full bg-sky-300"></div></div>
      <p class="mt-2 text-xs text-sky-100">$13,600 to go, 9 days left</p>
    </dd>
  </div>
</dl>

Grids, clickable cards and hover effects

Grids that choose their own column count, cards that adapt to their container instead of the viewport, a fully clickable card with a stretched link, group-hover lift and zoom effects and a card with a Popover API actions menu.

Card grid that picks its own columns

  • grid-cols-[repeat(auto-fill,minmax(16rem,1fr))]
  • h-full flex-col
  • mt-auto
  • line-clamp-2

Hover the cards, Tab through them or open the menu.

One arbitrary value, grid-cols-[repeat(auto-fill,minmax(16rem,1fr))], replaces a ladder of breakpoint classes: the grid fits as many 16rem columns as the row allows, three here and one on a phone. Every card is h-full flex flex-col, so cards in a row match heights and mt-auto pins each footer to the bottom, while line-clamp-2 keeps long titles in check.

View code
<ul role="list" class="grid w-full max-w-5xl grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-5">
  <li>
    <article class="flex h-full flex-col rounded-2xl bg-white p-5 ring-1 ring-indigo-950/10">
      <span class="grid size-10 place-items-center rounded-lg bg-indigo-50 text-indigo-700"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M9 9v11"/></svg></span>
      <h3 class="mt-4 line-clamp-2 font-semibold text-slate-900">Layout with CSS Grid</h3>
      <p class="mt-1 line-clamp-3 text-sm/6 text-slate-600">Tracks, gaps, named areas and the fr unit, with a gallery and a dashboard to rebuild.</p>
      <p class="mt-auto flex justify-between border-t border-slate-100 pt-4 text-xs font-medium text-slate-600"><span>12 lessons</span><span>2 h 40 min</span></p>
    </article>
  </li>
  <li>
    <article class="flex h-full flex-col rounded-2xl bg-white p-5 ring-1 ring-indigo-950/10">
      <span class="grid size-10 place-items-center rounded-lg bg-rose-50 text-rose-700"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5v17M3.5 12h17"/></svg></span>
      <h3 class="mt-4 line-clamp-2 font-semibold text-slate-900">Color systems that survive dark mode, brand refreshes and a growing component library</h3>
      <p class="mt-1 line-clamp-3 text-sm/6 text-slate-600">Build a palette in OKLCH, map it to tokens and test every pair for contrast.</p>
      <p class="mt-auto flex justify-between border-t border-slate-100 pt-4 text-xs font-medium text-slate-600"><span>9 lessons</span><span>1 h 55 min</span></p>
    </article>
  </li>
  <li>
    <article class="flex h-full flex-col rounded-2xl bg-white p-5 ring-1 ring-indigo-950/10">
      <span class="grid size-10 place-items-center rounded-lg bg-amber-50 text-amber-700"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M4 12h10M4 17h7"/></svg></span>
      <h3 class="mt-4 line-clamp-2 font-semibold text-slate-900">Type scales</h3>
      <p class="mt-1 line-clamp-3 text-sm/6 text-slate-600">Pick a ratio, set line heights and make headings fluid with clamp() without breaking zoom. Includes a long worked example for an editorial site with three heading levels, pull quotes and captions.</p>
      <p class="mt-auto flex justify-between border-t border-slate-100 pt-4 text-xs font-medium text-slate-600"><span>7 lessons</span><span>1 h 10 min</span></p>
    </article>
  </li>
  <li>
    <article class="flex h-full flex-col rounded-2xl bg-white p-5 ring-1 ring-indigo-950/10">
      <span class="grid size-10 place-items-center rounded-lg bg-emerald-50 text-emerald-700"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>
      <h3 class="mt-4 line-clamp-2 font-semibold text-slate-900">Accessible forms</h3>
      <p class="mt-1 line-clamp-3 text-sm/6 text-slate-600">Labels, errors, focus order and the ARIA you actually need.</p>
      <p class="mt-auto flex justify-between border-t border-slate-100 pt-4 text-xs font-medium text-slate-600"><span>10 lessons</span><span>2 h 05 min</span></p>
    </article>
  </li>
</ul>

Card that adapts to its container

  • @container
  • @md:flex-row
  • @md:w-2/5
  • md:grid-cols-[2fr_1fr]

Hover the cards, Tab through them or open the menu.

Both columns hold the same card markup. Each column is marked @container, and the card switches to a row with @md:flex-row once its container reaches 28rem, so on desktop the main column shows a horizontal card while the narrow sidebar keeps it stacked. Viewport breakpoints could not do this, because both cards share one viewport.

View code
<div class="grid w-full max-w-5xl gap-6 md:grid-cols-[2fr_1fr]">
  <div class="@container">
    <article class="tcd-cq flex flex-col overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-cyan-950/10 @md:flex-row">
      <img src="highland-road.webp" width="800" height="600" alt="Narrow road winding between steep green ridges under a grey sky" class="aspect-video w-full object-cover @md:aspect-auto @md:w-2/5">
      <div class="p-5">
        <h3 class="font-semibold text-slate-900">Trail report: the ridge road</h3>
        <p class="mt-1 text-sm/6 text-slate-600">Gravel from the second gate, fog most mornings, two passing places in the last 3 km.</p>
        <a href="#" class="mt-3 inline-block rounded-sm text-sm font-semibold text-cyan-800 hover:text-cyan-950 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-700">Read report</a>
      </div>
    </article>
    <p class="mt-2 font-mono text-xs text-slate-500">main column</p>
  </div>
  <div class="@container">
    <article class="tcd-cq flex flex-col overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-cyan-950/10 @md:flex-row">
      <img src="highland-road.webp" width="800" height="600" alt="Narrow road winding between steep green ridges under a grey sky" class="aspect-video w-full object-cover @md:aspect-auto @md:w-2/5">
      <div class="p-5">
        <h3 class="font-semibold text-slate-900">Trail report: the ridge road</h3>
        <p class="mt-1 text-sm/6 text-slate-600">Gravel from the second gate, fog most mornings, two passing places in the last 3 km.</p>
        <a href="#" class="mt-3 inline-block rounded-sm text-sm font-semibold text-cyan-800 hover:text-cyan-950 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-700">Read report</a>
      </div>
    </article>
    <p class="mt-2 font-mono text-xs text-slate-500">sidebar</p>
  </div>
</div>

Fully clickable card with a stretched link

  • after:absolute after:inset-0
  • has-[a:focus-visible]:outline-2
  • relative z-10
  • aria-pressed:

Press Tab to focus the card, then the bookmark.

The title link is the only link, and after:absolute after:inset-0 stretches its hit area over the relative article. has-[a:focus-visible]:outline-2 draws the focus ring around the whole card instead of the title text, and the bookmark button sits above the overlay with relative z-10, so its clicks are not swallowed. The script only flips aria-pressed on the bookmark.

View code
<article class="relative w-full max-w-md overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-rose-950/10 hover:shadow-md has-[a:focus-visible]:outline-2 has-[a:focus-visible]:outline-offset-4 has-[a:focus-visible]:outline-rose-700 motion-safe:transition-shadow">
  <img src="brick-alley.webp" width="800" height="600" alt="" class="aspect-[16/9] w-full object-cover">
  <div class="p-5">
    <div class="flex items-start justify-between gap-3">
      <h3 class="text-lg/snug font-semibold text-slate-900"><a href="#" id="tcd-story" class="after:absolute after:inset-0 hover:text-rose-800 focus-visible:outline-hidden">The alleys that kept the old city's brick</a></h3>
      <button type="button" id="tcd-bookmark" aria-pressed="false" aria-label="Bookmark story" class="relative z-10 grid size-11 shrink-0 cursor-pointer place-items-center rounded-full text-slate-500 hover:bg-rose-50 hover:text-rose-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-700 aria-pressed:text-rose-700"><svg class="size-5 fill-none in-aria-pressed:fill-current" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M7 4h10v16l-5-4-5 4z"/></svg></button>
    </div>
    <p class="mt-2 text-sm/6 text-slate-600">Fire codes, cheap rent and a stubborn preservation board explain why three blocks of service lanes look the way they did in 1910.</p>
    <p class="mt-4 text-sm text-slate-600">Urban history · <time datetime="2026-10-20">Oct 20</time></p>
  </div>
</article>

Lift and zoom on hover, motion-safe

  • motion-safe:hover:-translate-y-1.5
  • motion-safe:group-hover:scale-110
  • group-has-[a:focus-visible]:
  • motion-safe:transition

Hover the cards, Tab through them or open the menu.

The card lifts with motion-safe:hover:-translate-y-1.5 and a deeper shadow, the photo zooms through motion-safe:group-hover:scale-110 inside an overflow-hidden frame and the arrow slides right. Every hover style has a has-[a:focus-visible] twin so keyboard users get the same feedback, and the motion-safe: prefix drops all movement for people who ask for reduced motion.

View code
<ul role="list" class="grid w-full max-w-4xl gap-5 sm:grid-cols-3">
  <li>
    <article class="group relative overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-zinc-900/5 hover:shadow-xl has-[a:focus-visible]:shadow-xl has-[a:focus-visible]:outline-2 has-[a:focus-visible]:outline-offset-2 has-[a:focus-visible]:outline-zinc-900 motion-safe:transition motion-safe:duration-300 motion-safe:hover:-translate-y-1.5 motion-safe:has-[a:focus-visible]:-translate-y-1.5">
      <div class="overflow-hidden"><img src="granite-valley.webp" width="800" height="600" alt="" class="aspect-[3/2] w-full object-cover motion-safe:transition-transform motion-safe:duration-500 motion-safe:group-hover:scale-110 motion-safe:group-has-[a:focus-visible]:scale-110"></div>
      <div class="flex items-end justify-between gap-3 p-4">
        <div>
          <h3 class="font-semibold text-zinc-900"><a href="#" class="after:absolute after:inset-0 focus-visible:outline-hidden">Granite Valley</a></h3>
          <p class="text-sm text-zinc-600">18 camps · river beach</p>
        </div>
        <svg class="size-5 shrink-0 text-zinc-500 group-hover:text-zinc-900 motion-safe:transition-transform motion-safe:group-hover:translate-x-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-5-5l5 5-5 5"/></svg>
      </div>
    </article>
  </li>
  <li>
    <article class="group relative overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-zinc-900/5 hover:shadow-xl has-[a:focus-visible]:shadow-xl has-[a:focus-visible]:outline-2 has-[a:focus-visible]:outline-offset-2 has-[a:focus-visible]:outline-zinc-900 motion-safe:transition motion-safe:duration-300 motion-safe:hover:-translate-y-1.5 motion-safe:has-[a:focus-visible]:-translate-y-1.5">
      <div class="overflow-hidden"><img src="forest-waterfall.webp" width="800" height="600" alt="" class="aspect-[3/2] w-full object-cover motion-safe:transition-transform motion-safe:duration-500 motion-safe:group-hover:scale-110 motion-safe:group-has-[a:focus-visible]:scale-110"></div>
      <div class="flex items-end justify-between gap-3 p-4">
        <div>
          <h3 class="font-semibold text-zinc-900"><a href="#" class="after:absolute after:inset-0 focus-visible:outline-hidden">Mossy Falls</a></h3>
          <p class="text-sm text-zinc-600">6 camps · gorge trail</p>
        </div>
        <svg class="size-5 shrink-0 text-zinc-500 group-hover:text-zinc-900 motion-safe:transition-transform motion-safe:group-hover:translate-x-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-5-5l5 5-5 5"/></svg>
      </div>
    </article>
  </li>
  <li>
    <article class="group relative overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-zinc-900/5 hover:shadow-xl has-[a:focus-visible]:shadow-xl has-[a:focus-visible]:outline-2 has-[a:focus-visible]:outline-offset-2 has-[a:focus-visible]:outline-zinc-900 motion-safe:transition motion-safe:duration-300 motion-safe:hover:-translate-y-1.5 motion-safe:has-[a:focus-visible]:-translate-y-1.5">
      <div class="overflow-hidden"><img src="red-rock-canyon.webp" width="800" height="600" alt="" class="aspect-[3/2] w-full object-cover motion-safe:transition-transform motion-safe:duration-500 motion-safe:group-hover:scale-110 motion-safe:group-has-[a:focus-visible]:scale-110"></div>
      <div class="flex items-end justify-between gap-3 p-4">
        <div>
          <h3 class="font-semibold text-zinc-900"><a href="#" class="after:absolute after:inset-0 focus-visible:outline-hidden">Red Rock Mesa</a></h3>
          <p class="text-sm text-zinc-600">11 camps · sunset rim</p>
        </div>
        <svg class="size-5 shrink-0 text-zinc-500 group-hover:text-zinc-900 motion-safe:transition-transform motion-safe:group-hover:translate-x-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-5-5l5 5-5 5"/></svg>
      </div>
    </article>
  </li>
</ul>

Project card with a Popover API actions menu

  • popovertarget
  • data-archived:opacity-60
  • inset-auto
  • aria-expanded

Open the menu with the three-dot button.

The three-dot button opens a native popover through popovertarget, so Escape and outside clicks close it without code, and the menu uses inset-auto m-0 so the script can place it under the button. Choosing Mark complete or Archive sets data attributes on the card, and data-archived:opacity-60 plus the status badge text show the result.

View code
<article id="tcd-project" class="w-full max-w-md self-start rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5 data-archived:opacity-60 sm:p-6">
  <div class="flex items-start justify-between gap-3">
    <div class="min-w-0">
      <h3 class="truncate font-semibold text-slate-900">Harbor Lights rebrand</h3>
      <p class="mt-0.5 text-sm text-slate-600">Due <time datetime="2026-11-20">Nov 20</time> · Brightwater Co.</p>
    </div>
    <button type="button" id="tcd-menu-btn" popovertarget="tcd-menu" aria-haspopup="menu" aria-expanded="false" aria-label="Project actions" class="grid size-11 shrink-0 cursor-pointer place-items-center rounded-lg text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 aria-expanded:bg-slate-100"><svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="12" cy="19" r="1.8"/></svg></button>
  </div>
  <div class="mt-5 flex items-center justify-between text-sm">
    <span id="tcd-status" class="rounded-full bg-blue-50 px-2.5 py-1 text-xs font-semibold text-blue-800">In progress</span>
    <span class="font-medium text-slate-700 tabular-nums"><span id="tcd-progress-num">68</span>%</span>
  </div>
  <div class="mt-2 h-2 rounded-full bg-slate-100" role="progressbar" aria-label="Project progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="68" id="tcd-progress"><div id="tcd-progress-bar" class="h-2 w-[68%] rounded-full bg-blue-600 data-done:w-full data-done:bg-emerald-600"></div></div>
  <div class="mt-5 flex items-center justify-between">
    <div class="flex -space-x-2" aria-hidden="true">
      <svg class="size-8 rounded-full ring-2 ring-white" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="#dbeafe"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" fill="#1e3a8a" font-family="system-ui, sans-serif">RT</text></svg>
      <svg class="size-8 rounded-full ring-2 ring-white" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="#e9d5ff"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" fill="#581c87" font-family="system-ui, sans-serif">NB</text></svg>
    </div>
    <p class="text-sm text-slate-600">14 of 21 tasks</p>
  </div>
  <p id="tcd-menu-msg" class="sr-only" aria-live="polite"></p>
  <div id="tcd-menu" popover role="menu" aria-label="Project actions" class="inset-auto m-0 w-52 rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-slate-900/10">
    <button type="button" role="menuitem" data-action="rename" class="tcd-menu-item flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm text-slate-800 hover:bg-slate-100 focus-visible:bg-slate-100 focus-visible:outline-none"><svg class="size-4 text-slate-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16v4z"/></svg>Rename</button>
    <button type="button" role="menuitem" data-action="complete" class="tcd-menu-item flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm text-slate-800 hover:bg-slate-100 focus-visible:bg-slate-100 focus-visible:outline-none"><svg class="size-4 text-slate-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>Mark complete</button>
    <button type="button" role="menuitem" data-action="archive" class="tcd-menu-item flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm text-slate-800 hover:bg-slate-100 focus-visible:bg-slate-100 focus-visible:outline-none"><svg class="size-4 text-slate-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="5" rx="1"/><path d="M5 9v10h14V9M10 13h4"/></svg>Archive</button>
    <div class="my-1 h-px bg-slate-200" role="separator"></div>
    <button type="button" role="menuitem" data-action="delete" class="tcd-menu-item flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm text-rose-700 hover:bg-rose-50 focus-visible:bg-rose-50 focus-visible:outline-none"><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg>Delete</button>
  </div>
</article>

Profiles, loading and dark mode

A profile card with an SVG initials avatar and a follow toggle, a skeleton placeholder driven by aria-busy and a set of cards that switches between light and dark with one class.

Profile card with an initials avatar

  • @theme --color-tcd-moss
  • -mt-12 ring-4
  • aria-pressed:
  • divide-x

Click Follow.

The moss green comes from three @theme tokens, which create bg-tcd-moss, text-tcd-moss-dark and the other color utilities. The SVG initials avatar overlaps the solid cover band with -mt-12 and a white ring-4, the stats row is a dl with divide-x, and the Follow button is an aria-pressed toggle whose pressed look comes from aria-pressed: utilities.

View code
<article class="w-full max-w-sm overflow-hidden rounded-3xl bg-white text-center shadow-sm ring-1 ring-tcd-moss/15">
  <div class="h-24 bg-tcd-moss"></div>
  <svg class="mx-auto -mt-12 size-24 rounded-full ring-4 ring-white" viewBox="0 0 96 96" role="img" aria-label="Ines Albrecht"><circle cx="48" cy="48" r="48" fill="#e7efe9"/><text x="48" y="58" text-anchor="middle" font-size="30" font-weight="600" fill="#2b4232" font-family="system-ui, sans-serif">IA</text></svg>
  <div class="px-6 pb-6">
    <h3 class="mt-3 text-lg font-semibold text-slate-900">Ines Albrecht</h3>
    <p class="text-sm text-slate-600">Senior product designer, Lisbon</p>
    <p class="mt-3 text-sm/6 text-slate-700">Designs booking flows for small hotels. Writes about forms, empty states and the details nobody tests.</p>
    <dl class="mt-5 grid grid-cols-3 divide-x divide-slate-200 rounded-xl bg-tcd-moss-soft py-3">
      <div><dt class="text-xs text-tcd-moss-dark">Projects</dt><dd class="font-semibold text-slate-900 tabular-nums">42</dd></div>
      <div><dt class="text-xs text-tcd-moss-dark">Followers</dt><dd id="tcd-followers" class="font-semibold text-slate-900 tabular-nums">1,284</dd></div>
      <div><dt class="text-xs text-tcd-moss-dark">Following</dt><dd class="font-semibold text-slate-900 tabular-nums">310</dd></div>
    </dl>
    <div class="mt-5 grid grid-cols-2 gap-3">
      <button type="button" id="tcd-follow" aria-pressed="false" class="min-h-11 cursor-pointer rounded-xl bg-tcd-moss px-4 text-sm font-semibold text-white hover:bg-tcd-moss-dark focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tcd-moss aria-pressed:bg-tcd-moss-soft aria-pressed:text-tcd-moss-dark aria-pressed:ring-1 aria-pressed:ring-tcd-moss/30 aria-pressed:ring-inset">Follow</button>
      <a href="#" class="grid min-h-11 place-items-center rounded-xl px-4 text-sm font-semibold text-slate-800 ring-1 ring-slate-300 ring-inset hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tcd-moss">Message</a>
    </div>
  </div>
</article>

Skeleton loading card

  • animate-pulse
  • motion-reduce:animate-none
  • group-aria-busy:
  • aria-live

Click Finish loading.

The card wrapper carries aria-busy="true" while it loads, and group-aria-busy:block and group-aria-busy:hidden swap the gray placeholder for the real content, so the script only changes one attribute. The placeholder pulses with animate-pulse, and motion-reduce:animate-none keeps it still for people who prefer reduced motion.

View code
<div class="w-full max-w-sm">
  <div id="tcd-feed" class="group" aria-busy="true" aria-live="polite">
    <div class="hidden overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-900/5 group-aria-busy:block" aria-hidden="true">
      <div class="animate-pulse motion-reduce:animate-none">
        <div class="aspect-[16/9] bg-gray-200"></div>
        <div class="space-y-3 p-5">
          <div class="h-4 w-3/4 rounded-md bg-gray-200"></div>
          <div class="h-3 w-full rounded-md bg-gray-200"></div>
          <div class="h-3 w-5/6 rounded-md bg-gray-200"></div>
          <div class="flex items-center gap-3 pt-3">
            <div class="size-9 rounded-full bg-gray-200"></div>
            <div class="h-3 w-28 rounded-md bg-gray-200"></div>
          </div>
        </div>
      </div>
    </div>
    <article class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-900/5 group-aria-busy:hidden">
      <img src="jellyfish.webp" width="800" height="600" alt="Orange jellyfish with long trailing tentacles in deep blue water" class="aspect-[16/9] w-full object-cover">
      <div class="p-5">
        <h3 class="font-semibold text-gray-900">Inside the new open-water gallery</h3>
        <p class="mt-1 text-sm/6 text-gray-600">The aquarium's tallest tank holds 400 jellyfish under a slow current and blue light.</p>
        <div class="mt-4 flex items-center gap-3">
          <svg class="size-9 shrink-0" viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="20" r="20" fill="#cffafe"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" fill="#155e75" font-family="system-ui, sans-serif">HN</text></svg>
          <p class="text-sm text-gray-700">Hana Novak · 4 min read</p>
        </div>
      </div>
    </article>
  </div>
  <button type="button" id="tcd-load" class="mt-4 min-h-11 w-full cursor-pointer rounded-xl bg-gray-900 px-4 text-sm font-semibold text-white hover:bg-gray-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900">Finish loading</button>
</div>

Cards with light and dark variants

  • dark:bg-zinc-900
  • dark:shadow-none
  • dark:outline-white/10
  • @custom-variant dark

Toggle the theme.

Every light value has a dark: partner, and the wrapper starts with the dark class, so the class-based @custom-variant applies the dark: styles to everything inside it. In dark mode the cards trade their shadow for dark:shadow-none and a faint dark:outline-white/10 edge, the same approach the Tailwind docs use. The button flips the class and aria-pressed.

View code
<div id="tcd-theme" class="dark w-full max-w-2xl rounded-3xl bg-zinc-100 p-4 dark:bg-zinc-950 sm:p-6">
  <div class="flex items-center justify-between gap-3">
    <h3 class="font-semibold text-zinc-900 dark:text-white">Workspace</h3>
    <button type="button" id="tcd-theme-btn" aria-pressed="true" class="inline-flex min-h-11 cursor-pointer items-center gap-2 rounded-full bg-white px-4 text-sm font-semibold text-zinc-800 shadow-xs ring-1 ring-zinc-300 hover:bg-zinc-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-900 dark:bg-zinc-800 dark:text-zinc-100 dark:ring-white/15 dark:hover:bg-zinc-700 dark:focus-visible:outline-white"><svg class="size-4 dark:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg><svg class="hidden size-4 dark:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z"/></svg>Dark mode</button>
  </div>
  <div class="mt-5 grid gap-4 sm:grid-cols-2">
    <article class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-zinc-900/5 dark:bg-zinc-900 dark:shadow-none dark:ring-0 dark:outline dark:-outline-offset-1 dark:outline-white/10">
      <img src="canal-houses.webp" width="800" height="600" alt="Canal lined with old gabled houses and moored boats" class="aspect-[16/9] w-full object-cover">
      <div class="p-5">
        <h4 class="font-semibold text-zinc-900 dark:text-white">Canal district walking map</h4>
        <p class="mt-1 text-sm/6 text-zinc-600 dark:text-zinc-400">Updated with the two new footbridges and the winter ferry times.</p>
        <p class="mt-3 text-xs font-medium text-zinc-500 dark:text-zinc-400">Edited 2 hours ago</p>
      </div>
    </article>
    <article class="flex flex-col rounded-2xl bg-white p-5 shadow-sm ring-1 ring-zinc-900/5 dark:bg-zinc-900 dark:shadow-none dark:ring-0 dark:outline dark:-outline-offset-1 dark:outline-white/10">
      <h4 class="font-semibold text-zinc-900 dark:text-white">Storage</h4>
      <p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">38.4 GB of 50 GB used</p>
      <div class="mt-4 h-2.5 rounded-full bg-zinc-200 dark:bg-white/10" role="progressbar" aria-label="Storage used" aria-valuemin="0" aria-valuemax="100" aria-valuenow="77"><div class="h-2.5 w-[77%] rounded-full bg-teal-600 dark:bg-teal-400"></div></div>
      <ul role="list" class="mt-5 space-y-2 text-sm">
        <li class="flex justify-between text-zinc-700 dark:text-zinc-300"><span>Photos</span><span class="tabular-nums">24.1 GB</span></li>
        <li class="flex justify-between text-zinc-700 dark:text-zinc-300"><span>Video</span><span class="tabular-nums">11.6 GB</span></li>
        <li class="flex justify-between text-zinc-700 dark:text-zinc-300"><span>Documents</span><span class="tabular-nums">2.7 GB</span></li>
      </ul>
      <a href="#" class="mt-auto inline-block self-start rounded-sm pt-5 text-sm font-semibold text-teal-700 hover:text-teal-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600 dark:text-teal-300 dark:hover:text-teal-200">Manage storage</a>
    </article>
  </div>
</div>

What Is a Tailwind Card?

In practice it’s a content container made from utility classes, holding an image, some text and an action inside one padded, rounded box.

There is nothing to import. The v1 documentation states that the framework ships no pre-designed card components, and the v4.3 utility reference lists none either (Tailwind Labs).

Because Tailwind is a utility-first framework, spacing, radius, shadow and border all live in the markup. You compose a card rather than enable one.

daisyUI goes the other way and adds a ready-made card class through a plugin. The hand-built version has one real advantage, which is that every value sits right there in the HTML where you can edit it.

Most cards break down into the same few parts.

  • The outer article or div carries the background, border, radius and shadow.
  • An img or a background-image div goes first so it runs edge to edge.
  • The title (an h2 or h3) and the description sit inside a padded wrapper.
  • A link, button or metadata line closes out the card at the bottom.

Markup order is also reading order and tab order, so the title link should come before any secondary action.

This structure also answers a wider card layout question, which is how to chunk related content so a grid of items stays scannable.

Preflight, Tailwind’s base reset, decides what you start with. Margins are gone, headings have no styling, images get display: block and max-width: 100%, and the border class draws a solid 1px line in currentColor (Tailwind Labs, Preflight docs).

How to Build a Basic Tailwind Card

Building one is mostly a matter of choosing the container, adding the surface classes, and then working inward toward the text and the action.

  1. Use an article for the container when the card is a self-contained item such as a post or product, and a div otherwise. Cap the width with max-w-sm, which is 24rem.
  2. Add the surface with bg-white, rounded-xl, border border-gray-200 and shadow-sm. Name the border color, because v4 draws borders in currentColor by default.
  3. Keep the image outside the padded wrapper so it runs edge to edge, and put overflow-hidden on the container so the image follows the rounded corners.
  4. Pad the body with p-6 and style the text. Preflight leaves headings unstyled, so the title needs its own size and weight, for example text-lg font-semibold.
  5. Add the action as a link or button. Hover and focus styles for it come further down.
  6. Test in Tailwind Play, the online editor at play.tailwindcss.com, or in a local file with the Play CDN script. The Play CDN is designed for development only, not for production (Tailwind CSS docs).

Steps 1 to 5 assume Tailwind is already installed. If it isn’t, set it up in the project first.

Put together, the markup looks like this.

<article class="max-w-sm overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
  <img class="aspect-video w-full object-cover" src="/img/lake.jpg" alt="Mountain lake at sunrise">
  <div class="p-6">
    <h3 class="text-lg font-semibold text-gray-900">Card title</h3>
    <p class="mt-2 text-sm text-gray-600">A short description that supports the title.</p>
    <a href="/post" class="mt-4 inline-block text-sm font-medium text-indigo-600 hover:text-indigo-500">Read more</a>
  </div>
</article>

Shadow depth is the value most people end up tuning by eye. Try raw values in a CSS box shadow generator and you’ll see which step (xs, sm, md or lg) fits. I tend to start at the light end and only go heavier if the card gets lost against the page.

Snippets copied from v3 tutorials render differently in v4 because the class names in the shadow and radius scales each shifted down one step (Tailwind upgrade guide).

Lookv3 classv4 class
Subtle shadowshadow-smshadow-xs
Default shadowshadowshadow-sm
Small radiusrounded-smrounded-xs
Default radiusroundedrounded-sm

The old names still exist in v4, but each one now points to the next size up. A v3 shadow-sm card therefore looks heavier in v4 until it is renamed to shadow-xs.

Vertical, Horizontal and Image Card Layouts

Stack the image above the text for a vertical card, and add flex at a breakpoint for a horizontal one. The parts stay the same either way. Only the container direction and the image sizing change.

Vertical card with a top image

The default layout needs no flex at all. The image comes first, the padded body second, and normal block flow does the rest.

Give the image w-full plus a fixed height or an aspect class. Put overflow-hidden rounded-xl on the container to clip the corners, and p-6 on a wrapper div around the body.

Preflight sets images to display: block, so no stray gap opens under the image (Tailwind Labs).

Horizontal card with a side image

Horizontal cards stack on mobile and sit side by side from the md breakpoint, which starts at 48rem (Tailwind CSS docs).

<article class="flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white md:flex-row">
  <img class="h-48 w-full object-cover md:h-auto md:w-48" src="/img/lake.jpg" alt="Mountain lake at sunrise">
  <div class="p-6">
    <h3 class="text-lg font-semibold text-gray-900">Card title</h3>
    <p class="mt-2 text-sm text-gray-600">Text sits beside the image from 48rem up.</p>
  </div>
</article>

The catch is that the image needs a fixed width on large screens and an explicit height on small ones, or object-cover has no box to crop against.

Fitting and clipping the image

Card images mostly come down to the object-fit utilities (Tailwind CSS docs).

ClassCSSResult on a card
object-coverobject-fit: coverFills the box, crops the edges
object-containobject-fit: containShows the whole image, leaves empty bars
object-fillobject-fit: fillStretches the image and distorts it

Fix the box first, then fill it. Setting the image’s aspect ratio with aspect-video gives a 16 / 9 box, and object-cover crops the photo to fit without distortion.

Be aware that overflow-hidden also cuts off anything a child draws outside the card, focus rings and child shadows included. The card’s own shadow isn’t clipped.

Hover, Focus and Transition Effects

For a lift, pair hover:shadow-md with transition-shadow. Put group on the card if children should react through group-hover:, and give every hover style a focus-visible: counterpart.

  • shadow-sm hover:shadow-md transition-shadow deepens the shadow without moving the layout.
  • For a zoom, use group-hover:scale-105 on the img, with overflow-hidden on its wrapper to crop the growth.
  • Adding group-hover:text-indigo-600 to the title changes its color whenever the card is hovered anywhere.

The Tailwind docs demonstrate the group pattern with a project card. Hovering it changes the icon stroke and both text elements together (Tailwind CSS docs).

With hand-written CSS card hover effects, every state gets bundled into one class, whereas Tailwind hangs each state on the element as a separate utility.

In v4 the hover variant applies only when the primary input device supports hover, through @media (hover: hover) (Tailwind upgrade guide). A tap on a phone will not trigger the lift, and the docs advise treating hover as an enhancement. Anything a user needs, such as a revealed button, requires a visible default state.

The focus-visible variant matches keyboard focus. The bare ring utility also changed in v4: it is now 1px in currentColor, down from 3px in blue-500, so write ring-3 ring-blue-500 to get the v3 look (Tailwind upgrade guide).

Wrap movement in motion-safe:, for example motion-safe:group-hover:scale-105, so users who request reduced motion get no zoom.

There’s one v4 trap here. If you write a custom transition list, name scale instead of transform, as in transition-[opacity,scale], or the scale change snaps instead of animating.

How to Make an Entire Card Clickable and Accessible

Put one link on the card title and stretch its hit area across the card with a pseudo-element. Wrapping the whole card in an anchor places headings, paragraphs and buttons inside a single link, and nested links are invalid HTML.

<article class="relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-indigo-600">
  <img class="aspect-video w-full object-cover" src="/img/lake.jpg" alt="">
  <div class="p-6">
    <h3 class="text-lg font-semibold text-gray-900">
      <a href="/post" class="after:absolute after:inset-0 focus:outline-hidden">Card title</a>
    </h3>
    <p class="mt-2 text-sm text-gray-600">A short description.</p>
  </div>
</article>

The after: variant adds empty content automatically, so after:absolute after:inset-0 is enough once the card has relative (Tailwind CSS docs). The empty alt on the image keeps the announcement to the title alone.

That gives each card one tab stop and one link announcement, and mouse and touch users hit the whole card. The cost is that text under the overlay can’t be selected with the mouse.

WCAG 2.2 keeps 2.4.7 Focus Visible at Level AA. Any keyboard operable interface needs a mode where the focus indicator is visible (W3C).

That is why the snippet draws the indicator on the card with focus-within, so it surrounds the whole clickable area. Focus indicators count as non-text content and fall under 1.4.11 Non-text Contrast, so check the outline’s color contrast against both the card and the page background.

Buttons or links inside the card need relative z-10 to sit above the overlay, otherwise the stretched link swallows their clicks.

Pick h2, h3 or h4 to match the page outline. Preflight leaves headings unstyled, so changing the level has no visual side effect.

For a grid of cards, use a ul with role="list". Preflight removes list styling, and VoiceOver stops announcing unstyled lists as lists without that role (Tailwind Labs, Preflight docs).

Native HTML covers this pattern, so skip ARIA labels on the link and let the visible title name it. ARIA belongs where native elements fall short.

Card Grids, Breakpoints and Container Queries

Start with one column, step up with md:grid-cols-2 and lg:grid-cols-3, and let gap-6 handle the spacing.

<ul role="list" class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
  <li><!-- card --></li>
  <li><!-- card --></li>
  <li><!-- card --></li>
</ul>

Tailwind is built around mobile-first design. Unprefixed utilities apply at every width, and prefixed ones apply at that breakpoint and above.

That is why sm: means “from the small breakpoint up”, never “on phones” (Tailwind CSS docs).

Grid cells stretch by default, so cards in one row share a height. The contents do not follow unless the card is a flex column.

Put h-full on each card so it fills its grid cell, and flex flex-col on the card so the body can stretch. Then mt-auto on the footer or action row pins it to the bottom.

For a grid that picks its own column count, use grid-cols-[repeat(auto-fill,minmax(16rem,1fr))]. It needs no breakpoints. Swap in auto-fit only if a single short row should stretch its cards across the full width, because auto-fit collapses the empty tracks that auto-fill keeps.

Container queries are part of Tailwind core since v4.0, released January 22, 2025, so the separate container-queries plugin is gone (Tailwind Labs).

Viewport-based media queries read the screen. A container query reads the parent, so one card can sit horizontal in a wide main column and stacked in a narrow sidebar.

<div class="@container">
  <article class="flex flex-col gap-4 @md:flex-row">
    <!-- image and body -->
  </article>

</div>

Default container sizes run from 16rem (@3xs) to 80rem (@7xl), and @md starts at 28rem (Tailwind CSS docs). The element marked @container cannot restyle itself. Only its descendants respond.

Dark Mode Styling for Cards

Every light color on the card needs a dark: partner, from the surface and text to the border. The default variant follows the operating system through prefers-color-scheme.

A typical set of pairs looks like this.

  • bg-white becomes dark:bg-gray-900 on the surface.
  • The title swaps text-gray-900 for dark:text-white.
  • Description text goes from text-gray-600 to dark:text-gray-400.
  • border-gray-200 turns into dark:border-gray-800.

Shadows nearly vanish on dark surfaces. The card in the Tailwind docs drops its shadow in dark mode and uses a faint white outline instead, through dark:shadow-none and dark:outline-white/10 (Tailwind CSS docs).

To switch themes with a button instead of the system setting, override the variant in your CSS.

@custom-variant dark (&:where(.dark, .dark *));

Now dark: utilities apply whenever a dark class sits higher in the tree, usually on the html element. A data attribute works the same way with [data-theme=dark] in place of the class.

For a light, dark and system choice, read localStorage and matchMedia in an inline script in the head. That stops the page from flashing the wrong theme on load (Tailwind CSS docs).

WCAG 1.4.3 requires a 4.5:1 contrast ratio for normal text and 3:1 for large text, meaning 18 point or 14 point bold (W3C).

Muted description text is the pair most likely to slip below that line, so test it instead of trusting the shade names. An accessible color palette generator checks light and dark pairs before they reach the markup.

Build Your Own Cards or Use a Component Library?

Build your own when the card is a signature element of the design, and use a library when the card has to match a large set of other components. For a single card, a library rarely earns its install.

Prebuilt card options compared

OptionCard approachCost or licenseSetup
Tailwind PlusUI Blocks as HTML, React or Vue markupOne-time paid license, closed to new customersCopy markup
FlowbiteStatic card markup, optional JS for interactive partsMITPlugin and JS file
daisyUIClasses: card, card-body, card-title, card-sideOpen sourcePlugin
shadcn/uiReact Card with CardHeader, CardContent, CardFooterOpen sourceCLI adds the component

daisyUI covers sizes from card-xs to card-xl and a card-side modifier for horizontal layouts. shadcn/ui installs through pnpm dlx shadcn@latest add card, lands at @/components/ui/card in your project, and takes a size="sm" prop plus a --card-spacing variable for density.

HyperUI is pure copy and paste. It has no installation and an MIT license (HyperUI README). Its component library at hyperui.dev lists card, blog card, product card and pricing sections (HyperUI). Preline UI offers free blocks under an MIT license and a Fair Use License, with a paid Pro tier above them (Preline README).

Tailwind Labs closed Tailwind Plus to new customers on September 9, 2026, the day it announced it is joining Shopify. Existing customers keep access to what they bought. The one-time price the official checkout listed before the closure was $299 for a personal license (Tailwind Labs), so it no longer applies to a new purchase. For a new project, pick one of the other options in the table.

Going with a library means matching buttons, modals and dropdowns come in the same set, and sizes, borders and horizontal layouts already exist as classes or props. You also take on an extra dependency, plus JavaScript for the interactive parts (Flowbite, Preline).

Going custom means no dependency, every value stays editable in the markup, and the card follows your own design tokens. Accessibility and dark mode testing are yours to do, though, and variants multiply unless the card is wrapped in a component.

Building a reusable card yourself

The Tailwind docs name components as the best strategy for reuse in React, Svelte and Vue, and template partials for Blade, ERB, Twig or Nunjucks (Tailwind CSS docs). If the project is React, the walkthrough on using Tailwind CSS in React covers setup before the component below.

export function Card({ title, href, tone = "plain", children }) {
  const tones = {
    plain: "border-gray-200 bg-white text-gray-900",
    inverted: "border-gray-800 bg-gray-900 text-white",
  };

  return (
  <article className={`relative rounded-xl border p-6 shadow-sm ${tones[tone]}`}>
  <h3 className="text-lg font-semibold">
  <a href={href} className="after:absolute after:inset-0">{title}</a>
  </h3>
  <div className="mt-2 text-sm">{children}</div>
  </article>
  );
}

Vue follows the same pattern in a single-file component. If its style block uses @apply, v4 needs an @reference to the main stylesheet, because separately bundled styles cannot see theme variables (Tailwind upgrade guide).

The docs’ own custom CSS example for a button class uses theme variables instead of @apply. Utilities in the template are simpler still.

IntelliSense in VS Code adds autocomplete, linting and hover previews for class names. The Prettier plugin sorts every class list into Tailwind’s recommended order, so copied cards produce clean diffs.

Where Tailwind Cards Break

Cards go wrong in fairly predictable places. String-built class names, snippets from older versions, browsers below the v4 floor, text that runs long, and extra classes passed in from outside cause most of the trouble.

FailureWhat you seeFix
Dynamic class namesA variant renders unstyledLook up complete class names in a map, or safelist with @source inline()
Stale snippetsOdd spacing, shadows or ringsCheck the loaded version, then run npx @tailwindcss/upgrade (Node.js 20+)
Old browsersv4 utilities have no effectStay on v3.4 or raise the support floor
Long textTitles wrap to many lines and cards grow unevenlyline-clamp-2 on the title, line-clamp-3 on the description
Classes passed from outsideAn override does nothingExpose props instead of accepting extra classes

Tailwind scans source files as plain text and cannot read string interpolation. A class like bg-${color}-600 never exists as a full string, so the CSS is never generated (Tailwind CSS docs).

Conflicts between classes resolve by stylesheet order, not attribute order. When two utilities target the same property, the later rule in the generated CSS wins.

Stale code is easy to find. Many card tutorials still load the prebuilt v1 or v2 stylesheets from a CDN, versions that predate v4 by years.

The two Play CDN scripts differ too. The v3 docs load cdn.tailwindcss.com, while v4 uses @tailwindcss/browser@4 (Tailwind CSS docs).

Tailwind CSS v4.0 targets Safari 16.4+, Chrome 111+ and Firefox 128+, and the upgrade guide tells projects that need older browsers to stay on v3.4 (Tailwind Labs). Check your cross-browser compatibility requirements before pasting a v4 card into a legacy site.

Tailwind Card FAQ

How do pricing, profile, blog and product cards differ?

Each type keeps the same container and swaps the content blocks. Blog cards lead with an image, date and excerpt, and product cards add a price and a cart button.

Pricing cards drop the image for a plan name, price and feature list. Profile cards center a rounded-full avatar above a name and role.

How does a Tailwind card compare with a Bootstrap card?

Bootstrap ships a card class with built-in parts, while Tailwind provides none and builds the card from utilities in the markup.

That makes a Bootstrap card quicker to start and a Tailwind card easier to restyle in place, because no component stylesheet needs overriding.

Can a card have a gradient border?

Yes. Wrap the card in a div with bg-linear-to-r from-indigo-500 to-pink-500 p-px rounded-xl, then give the inner card a solid background and a slightly smaller radius, such as rounded-[11px].

The 1px of padding shows the gradient as a border. Tailwind v4 renamed the older bg-gradient-to-r class to bg-linear-to-r (Tailwind Labs, 2025).

How do I build a flip card with Tailwind?

Put perspective-distant on the wrapper, transform-3d and transition-transform on the inner box, and group-hover:rotate-y-180 to turn it. Both faces need backface-hidden, and the back face starts at rotate-y-180.

In v4, hover applies only on hover-capable devices, so add a focus or tap toggle for phones. A plain CSS flip card follows the same structure.

How do I truncate long text in a card?

Use truncate for a single line and line-clamp-3 for three lines. Both end the cut text with an ellipsis.

Inside a flex row, add min-w-0 to the text container. Without it, the text keeps its full width and never truncates.

Building a Tailwind Card in the Right Order

Decisions hold up best in a fixed sequence, with markup first, then layout, and decoration at the end.

Semantic elements and heading level change the HTML structure, so fixing them late means rewriting the card. Shadow, radius and color classes swap in seconds at any stage.

  • Semantic element, heading level and link pattern
  • Flex or grid layout, with a breakpoint or container query
  • Hover, focus and dark variants last

The trade-off is a plain first render. The card looks unfinished until the third step, so judge it by keyboard behavior before appearance.

This order was checked against the Tailwind v4.3 documentation on October 29, 2026. A future major release that renames utilities, as v4 did with shadow-sm and rounded-sm, would change class names but leave the sequence intact.