Pure TailwindMIT licensed

Tailwind Horizontal Product Card with Container Queries

The cart-row card that responds to its own width instead of the viewport's — and in v4 that is a first-class variant, not a raw @container block. @container/card on the wrapper plus @max-\[420px\]/card:flex-col on the card is the entire responsive story, so the same component works in a wide cart column and a narrow sidebar.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tpc-11 w-full min-h-svh grid place-items-center bg-tpc11-bg font-tpc11-sans px-5 py-12" aria-label="Horizontal container-query product card demo">
  <div class="@container/card w-[min(520px,100%)]">
    <article class="flex rounded-2xl overflow-hidden bg-tpc11-card shadow-[0_14px_36px_-20px_oklch(0.25_0.03_260/0.4)] @max-[420px]/card:flex-col">
      <div role="img" aria-label="Product photo: Ridge insulated bottle" class="flex-[0_0_40%] aspect-square bg-linear-[135deg,var(--color-tpc11-ph),var(--color-tpc11-ph2)] bg-[url('https://images.unsplash.com/photo-1602143407151-7111542de6e8?w=700&h=700&fit=crop&q=70')] bg-cover bg-center @max-[420px]/card:flex-none @max-[420px]/card:w-full @max-[420px]/card:aspect-[16/9]"></div>
      <div class="flex-1 flex flex-col justify-between gap-4.5 px-5.5 py-5">
        <div>
          <h3 class="text-[17px] font-semibold leading-[1.35] text-tpc11-ink text-pretty">Ridge Insulated Bottle &middot; 750ml</h3>
          <p class="mt-1 text-[13px] text-tpc11-mut">Slate &middot; Keeps cold 24h</p>
        </div>
        <div class="flex flex-wrap items-center justify-between gap-3">
          <p class="text-[20px] font-extrabold text-tpc11-ink tabular-nums">$35</p>
          <div class="flex gap-2">
            <button type="button" class="min-h-11 h-11 px-4 rounded-[10px] border border-tpc11-edge bg-tpc11-card text-tpc11-ghostink text-[13px] font-semibold cursor-pointer transition-colors duration-150 hover:border-tpc11-acc hover:text-tpc11-acc focus-visible:outline-3 focus-visible:outline-[color-mix(in_oklch,var(--color-tpc11-acc)_45%,oklch(1_0_0))] focus-visible:outline-offset-2 motion-reduce:transition-none">Save</button>
            <button type="button" class="min-h-11 h-11 px-4.5 rounded-[10px] border-0 bg-tpc11-acc text-tpc11-onacc text-[13px] font-semibold cursor-pointer transition-colors duration-150 hover:bg-tpc11-acc2 focus-visible:outline-3 focus-visible:outline-[color-mix(in_oklch,var(--color-tpc11-acc)_45%,oklch(1_0_0))] focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
          </div>
        </div>
      </div>
    </article>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tpc11-bg: #eef1f5;
  --color-tpc11-bg: oklch(0.95 0.008 250);
  --color-tpc11-card: #ffffff;
  --color-tpc11-card: oklch(1 0 0);
  --color-tpc11-ink: #101828;
  --color-tpc11-ink: oklch(0.23 0.03 260);
  --color-tpc11-mut: #5b6678;
  --color-tpc11-mut: oklch(0.5 0.025 260);
  --color-tpc11-ghostink: #344054;
  --color-tpc11-ghostink: oklch(0.4 0.03 260);
  --color-tpc11-edge: #d0d5dd;
  --color-tpc11-edge: oklch(0.86 0.012 260);
  --color-tpc11-ph: #dbe3ee;
  --color-tpc11-ph: oklch(0.9 0.02 250);
  --color-tpc11-ph2: #c9d5e6;
  --color-tpc11-ph2: oklch(0.85 0.03 250);
  --color-tpc11-acc: #2f5fbf;
  --color-tpc11-acc: oklch(0.52 0.14 260);
  --color-tpc11-acc2: #26509f;
  --color-tpc11-acc2: oklch(0.45 0.13 260);
  --color-tpc11-onacc: #ffffff;
  --color-tpc11-onacc: oklch(1 0 0);
  --font-tpc11-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tpc-11 {
  width: 100%;
  min-height: 100svh;
}

.tpc-11 ::selection {
  background: var(--color-tpc11-acc);
  color: #fff;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tpc11-bg: #eef1f5;
  --color-tpc11-bg: oklch(0.95 0.008 250);
  --color-tpc11-card: #ffffff;
  --color-tpc11-card: oklch(1 0 0);
  --color-tpc11-ink: #101828;
  --color-tpc11-ink: oklch(0.23 0.03 260);
  --color-tpc11-mut: #5b6678;
  --color-tpc11-mut: oklch(0.5 0.025 260);
  --color-tpc11-ghostink: #344054;
  --color-tpc11-ghostink: oklch(0.4 0.03 260);
  --color-tpc11-edge: #d0d5dd;
  --color-tpc11-edge: oklch(0.86 0.012 260);
  --color-tpc11-ph: #dbe3ee;
  --color-tpc11-ph: oklch(0.9 0.02 250);
  --color-tpc11-ph2: #c9d5e6;
  --color-tpc11-ph2: oklch(0.85 0.03 250);
  --color-tpc11-acc: #2f5fbf;
  --color-tpc11-acc: oklch(0.52 0.14 260);
  --color-tpc11-acc2: #26509f;
  --color-tpc11-acc2: oklch(0.45 0.13 260);
  --color-tpc11-onacc: #ffffff;
  --color-tpc11-onacc: oklch(1 0 0);
  --font-tpc11-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tpc-11 {
  width: 100%;
  min-height: 100svh;
}

.tpc-11 ::selection {
  background: var(--color-tpc11-acc);
  color: #fff;
}
3Fonts used by this demooptional — skip if you have your own type stack
<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=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • This is the port with the biggest structural win. A wrapper rule plus an @container block become @container/card and two @max-[420px]/card: variants, and the responsive decision moves onto the elements that actually change.
  • The container is named. v4 supports unnamed @max-[…]: variants, but a reusable card that might be nested inside another container needs the explicit reference or it silently answers to the wrong box.
  • flex-[0_0_40%] stays an arbitrary value: the three-part shorthand is clearer as one unit than grow-0 shrink-0 basis-[40%], and it is how the original expressed it.
  • The stacked state needs three variants, not one — flex-col on the card, flex-none w-full and a new aspect-[16/9] on the media. The CSS version's two rules hid that third change inside a shorthand.
  • Enhancement over the CSS original: both buttons grew to min-h-11, the muted text was darkened to clear 4.5:1, and the footer gained flex-wrap so a long price and two buttons break gracefully instead of overflowing.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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 Horizontal Product Card with Container Queries
Source: https://codefronts.com/components/tailwind-css-product-cards/tailwind-horizontal-product-card-container-queries/

The cart-row card that responds to its own width instead of the viewport's — and in v4 that is a first-class variant, not a raw @container block. @container/card on the wrapper plus @max-\[420px\]/card:flex-col on the card is the entire responsive story, so the same component works in a wide cart column and a narrow sidebar.
## HTML
```html
<section class="tpc-11 w-full min-h-svh grid place-items-center bg-tpc11-bg font-tpc11-sans px-5 py-12" aria-label="Horizontal container-query product card demo">
  <div class="@container/card w-[min(520px,100%)]">
    <article class="flex rounded-2xl overflow-hidden bg-tpc11-card shadow-[0_14px_36px_-20px_oklch(0.25_0.03_260/0.4)] @max-[420px]/card:flex-col">
      <div role="img" aria-label="Product photo: Ridge insulated bottle" class="flex-[0_0_40%] aspect-square bg-linear-[135deg,var(--color-tpc11-ph),var(--color-tpc11-ph2)] bg-[url('https://images.unsplash.com/photo-1602143407151-7111542de6e8?w=700&h=700&fit=crop&q=70')] bg-cover bg-center @max-[420px]/card:flex-none @max-[420px]/card:w-full @max-[420px]/card:aspect-[16/9]"></div>
      <div class="flex-1 flex flex-col justify-between gap-4.5 px-5.5 py-5">
        <div>
          <h3 class="text-[17px] font-semibold leading-[1.35] text-tpc11-ink text-pretty">Ridge Insulated Bottle &middot; 750ml</h3>
          <p class="mt-1 text-[13px] text-tpc11-mut">Slate &middot; Keeps cold 24h</p>
        </div>
        <div class="flex flex-wrap items-center justify-between gap-3">
          <p class="text-[20px] font-extrabold text-tpc11-ink tabular-nums">$35</p>
          <div class="flex gap-2">
            <button type="button" class="min-h-11 h-11 px-4 rounded-[10px] border border-tpc11-edge bg-tpc11-card text-tpc11-ghostink text-[13px] font-semibold cursor-pointer transition-colors duration-150 hover:border-tpc11-acc hover:text-tpc11-acc focus-visible:outline-3 focus-visible:outline-[color-mix(in_oklch,var(--color-tpc11-acc)_45%,oklch(1_0_0))] focus-visible:outline-offset-2 motion-reduce:transition-none">Save</button>
            <button type="button" class="min-h-11 h-11 px-4.5 rounded-[10px] border-0 bg-tpc11-acc text-tpc11-onacc text-[13px] font-semibold cursor-pointer transition-colors duration-150 hover:bg-tpc11-acc2 focus-visible:outline-3 focus-visible:outline-[color-mix(in_oklch,var(--color-tpc11-acc)_45%,oklch(1_0_0))] focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
          </div>
        </div>
      </div>
    </article>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tpc11-bg: #eef1f5;
  --color-tpc11-bg: oklch(0.95 0.008 250);
  --color-tpc11-card: #ffffff;
  --color-tpc11-card: oklch(1 0 0);
  --color-tpc11-ink: #101828;
  --color-tpc11-ink: oklch(0.23 0.03 260);
  --color-tpc11-mut: #5b6678;
  --color-tpc11-mut: oklch(0.5 0.025 260);
  --color-tpc11-ghostink: #344054;
  --color-tpc11-ghostink: oklch(0.4 0.03 260);
  --color-tpc11-edge: #d0d5dd;
  --color-tpc11-edge: oklch(0.86 0.012 260);
  --color-tpc11-ph: #dbe3ee;
  --color-tpc11-ph: oklch(0.9 0.02 250);
  --color-tpc11-ph2: #c9d5e6;
  --color-tpc11-ph2: oklch(0.85 0.03 250);
  --color-tpc11-acc: #2f5fbf;
  --color-tpc11-acc: oklch(0.52 0.14 260);
  --color-tpc11-acc2: #26509f;
  --color-tpc11-acc2: oklch(0.45 0.13 260);
  --color-tpc11-onacc: #ffffff;
  --color-tpc11-onacc: oklch(1 0 0);
  --font-tpc11-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tpc-11 {
  width: 100%;
  min-height: 100svh;
}

.tpc-11 ::selection {
  background: var(--color-tpc11-acc);
  color: #fff;
}
```

How this works

The container is a utility now. The CSS version needs a wrapper rule (container-type: inline-size) plus a separate @container (max-width: 420px) block. v4 collapses both into the markup:

<div class="@container/card w-[min(520px,100%)]">
  <article class="flex @max-[420px]/card:flex-col">
    <div class="flex-[0_0_40%] aspect-square @max-[420px]/card:aspect-[16/9]">

Both ends of the responsive decision now sit on the element that changes, which is the whole argument for container queries in the first place: the component knows how to be narrow, and the page does not have to tell it.

Why the name matters. @container/card names this container, and @max-[420px]/card: targets it explicitly. Drop this card into a container-queried grid and the unnamed form would resolve against the nearest container — which might be the grid, not the card.

Where it degrades. Without container-query support the card stays in its horizontal layout at every width, which is why the media is flex-[0_0_40%] rather than a fixed pixel width: even unstacked, it never squeezes the body to nothing.

Make it yours

  • Stack threshold: the 420px inside both @max-[…]/card: variants. Tune it to where your image gets too small to read.
  • Media share: flex-[0_0_40%] → flex-[0_0_28%] for a thumbnail row, 50% for a feature row.
  • Swap the actions for your cart's real controls — a quantity stepper (see demo 16), remove, save-for-later.
  • Add @min-[560px]/card: variants to grow the type when the card is given a wide column.
  • Retheme through --color-tpc11-acc: the ghost button's hover, the CTA fill and both focus rings read it.

Gotchas — read before shipping

  • A container query cannot read the element that declares the container — @container/card goes on the wrapper, and the card inside is what responds.
  • Name your containers. An unnamed @max-[420px]: resolves against the nearest container ancestor, which changes meaning the moment this card is nested.
  • Give the media a fixed aspect-* in both states, or rows in a cart list jog as images load.
  • Icon-only cart controls need aria-label. The two buttons here are text, which is the easier correct answer.
  • Container queries force layout containment on the inline axis — a child with a percentage height inside will not resolve the way it does in a normal flex row.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Container queries: Chrome 105+, Safari 16+, Firefox 110+. Where unsupported the card keeps its horizontal layout at all widths, which is a usable fallback rather than a broken one.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…