Product gallery
The top of a product page: a swipeable photo gallery with dots on a phone and thumbnails from tablet up, name, sale price, rating, colour swatches, size chips with one sold out, Add to bag and save.
Delivering to Canal Street
It is live: hover, focus and its own controls respond. Actions that would change data (approve, send, delete) are for your app to wire. The width is a real 390 px, 768 px or 1280 px box, shrunk to fit your screen.
<!-- Icons: Lucide (lucide.dev), ISC licence -->
<style>
.odl-restaurant-card { box-sizing: border-box; inline-size: 100%; max-inline-size: 64rem; margin-inline: auto; container-type: inline-size; color: var(--ds-color-text-primary); font: var(--ds-text-body-md); text-wrap: pretty; }
.odl-restaurant-card *, .odl-restaurant-card *::before, .odl-restaurant-card *::after { box-sizing: border-box; }
.odl-restaurant-card .odl-wrap { display: grid; gap: var(--ds-space-stack-lg); padding: var(--ds-space-inset-lg); }
@container (min-width: 40rem) { .odl-restaurant-card .odl-wrap { padding: var(--ds-space-inset-xl); } }
.odl-restaurant-card .odl-top { display: grid; gap: var(--ds-space-stack-xs); }
.odl-restaurant-card h3 { margin: 0; font: var(--ds-text-title-lg); }
.odl-restaurant-card .odl-top p { margin: 0; color: var(--ds-color-text-secondary); font: var(--ds-text-label-md); }
.odl-restaurant-card ul { display: grid; gap: var(--ds-space-stack-xl) var(--ds-space-inset-xl); margin: 0; padding: 0; list-style: none; }
@container (min-width: 36rem) { .odl-restaurant-card ul { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container (min-width: 54rem) { .odl-restaurant-card ul { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.odl-restaurant-card li { position: relative; min-inline-size: 0; inline-size: 100%; container-type: inline-size; display: grid; align-content: start; gap: var(--ds-space-stack-md); }
.odl-restaurant-card .odl-cover { position: relative; min-inline-size: 0; aspect-ratio: 16 / 10; border-radius: var(--ds-radius-container); overflow: hidden; background: var(--ds-color-surface-sunken); }
.odl-restaurant-card .odl-cover img { display: block; inline-size: 100%; max-inline-size: 100%; block-size: 100%; object-fit: cover; transition: transform var(--ds-motion-duration-medium) var(--ds-motion-easing-standard); }
.odl-restaurant-card li:hover .odl-cover img { transform: scale(1.03); }
.odl-restaurant-card .odl-fav { --odl-hang: calc((var(--ds-size-target-touch) - var(--ds-size-control-md)) / 2); position: absolute; z-index: 1; inset-block-start: calc(var(--ds-space-inset-sm) - var(--odl-hang)); inset-inline-end: calc(var(--ds-space-inset-sm) - var(--odl-hang));
display: grid; place-items: center; inline-size: max(var(--ds-size-target-touch), var(--ds-size-control-md)); block-size: max(var(--ds-size-target-touch), var(--ds-size-control-md)); padding: 0; border: 0; border-radius: var(--ds-radius-full); background: transparent; color: var(--ds-color-text-primary); cursor: pointer; }
.odl-restaurant-card .odl-fav span { display: grid; place-items: center; inline-size: var(--ds-size-control-md); block-size: var(--ds-size-control-md); border-radius: var(--ds-radius-full); background: var(--ds-color-surface-raised); box-shadow: var(--ds-elevation-raised);
transition: transform var(--ds-motion-duration-short) var(--ds-motion-easing-standard); }
.odl-restaurant-card .odl-fav:active span { transform: scale(var(--ds-motion-scale-press)); }
.odl-restaurant-card .odl-fav svg { inline-size: var(--ds-size-icon-sm); block-size: var(--ds-size-icon-sm); stroke-width: var(--ds-icon-stroke-md); }
.odl-restaurant-card .odl-fav[aria-pressed="true"] { color: var(--ds-color-icon-accent); }
.odl-restaurant-card .odl-fav[aria-pressed="true"] path { fill: currentColor; }
.odl-restaurant-card .odl-info { display: grid; gap: var(--ds-space-stack-xs); }
.odl-restaurant-card .odl-row { display: flex; align-items: center; justify-content: space-between; gap: var(--ds-space-inline-md); }
.odl-restaurant-card h4 { margin: 0; min-inline-size: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font: var(--ds-text-title-sm); }
.odl-restaurant-card h4 a { color: inherit; text-decoration: none; }
.odl-restaurant-card h4 a::after { content: ""; position: absolute; inset: 0; }
.odl-restaurant-card h4 a:focus-visible { outline: none; }
.odl-restaurant-card li:has(h4 a:focus-visible) .odl-cover { outline: var(--ds-focus-ring-width) solid var(--ds-color-border-focus); outline-offset: var(--ds-focus-ring-offset); }
.odl-restaurant-card .odl-rating { flex: none; display: inline-flex; align-items: center; gap: var(--ds-space-inline-xs); font: var(--ds-text-label-md); font-variant-numeric: tabular-nums; }
.odl-restaurant-card .odl-rating svg { inline-size: var(--ds-size-icon-sm); block-size: var(--ds-size-icon-sm); stroke-width: var(--ds-icon-stroke-md); }
.odl-restaurant-card .odl-count { color: var(--ds-color-text-tertiary); }
@container (max-width: 16rem) { .odl-restaurant-card .odl-count { display: none; } }
.odl-restaurant-card .odl-cuisine { margin: 0; color: var(--ds-color-text-secondary); font: var(--ds-text-body-md); }
.odl-restaurant-card .odl-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ds-space-stack-xs) var(--ds-space-inset-lg); margin: 0; color: var(--ds-color-text-secondary); font: var(--ds-text-label-md); font-variant-numeric: tabular-nums; }
.odl-restaurant-card .odl-meta span { display: inline-flex; align-items: center; gap: var(--ds-space-inline-xs); white-space: nowrap; }
.odl-restaurant-card .odl-meta svg, .odl-restaurant-card .odl-promo svg { flex: none; inline-size: var(--ds-size-icon-sm); block-size: var(--ds-size-icon-sm); stroke-width: var(--ds-icon-stroke-md); color: var(--ds-color-icon-subtle); }
.odl-restaurant-card .odl-promo { display: flex; align-items: center; gap: var(--ds-space-inline-xs); justify-self: start; max-inline-size: 100%; margin: var(--ds-space-stack-xs) 0 0; padding: var(--ds-space-inset-xs) var(--ds-space-inset-sm); border-radius: var(--ds-radius-control-sm);
background: var(--ds-color-bg-neutral-subtle); color: var(--ds-color-text-primary); font: var(--ds-text-label-md); }
.odl-restaurant-card .odl-promo { align-items: flex-start; }
.odl-restaurant-card .odl-promo svg { margin-block-start: var(--ds-space-2); }
.odl-restaurant-card button:focus-visible { outline: none; }
.odl-restaurant-card button:focus-visible span { outline: var(--ds-focus-ring-width) solid var(--ds-color-border-focus); outline-offset: var(--ds-focus-ring-offset); }
.odl-restaurant-card .odl-sr { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .odl-restaurant-card, .odl-restaurant-card *, .odl-restaurant-card *::before { transition: none !important; animation: none !important; } }
</style>
<section class="odl-restaurant-card" aria-labelledby="odl-rc-title">
<div class="odl-wrap">
<div class="odl-top"><h3 id="odl-rc-title">Near you</h3><p>Delivering to Canal Street</p></div>
<ul>
<li>
<div class="odl-cover">
<img src="https://images.unsplash.com/photo-1583354608715-177553a4035e?w=720&h=450&fit=crop&auto=format&q=70" alt="A café with a white round table and chairs" width="360" height="225" loading="lazy" decoding="async">
<button type="button" class="odl-fav" aria-label="Save Hollinby Bakehouse" aria-pressed="true"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg></span></button>
</div>
<div class="odl-info">
<div class="odl-row"><h4><a href="#">Hollinby Bakehouse</a></h4><span class="odl-rating"><svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.010a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/></svg><span class="odl-sr">Rated </span>4.8<span class="odl-sr"> out of 5,</span> <span class="odl-count">(1.2k)</span></span></div>
<p class="odl-cuisine">Bakery · Breakfast · $$</p>
<p class="odl-meta"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>15–25 min</span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18.5" cy="17.5" r="3.5"/><circle cx="5.5" cy="17.5" r="3.5"/><circle cx="15" cy="5" r="1"/><path d="M12 17.5V14l-3-3 4-3 2 3h2"/></svg>Free delivery</span></p>
<p class="odl-promo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/></svg>20% off pastries before 11 am</p>
</div>
</li>
<li>
<div class="odl-cover">
<img src="https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=720&h=450&fit=crop&auto=format&q=70" alt="A bowl of mixed vegetable salad" width="360" height="225" loading="lazy" decoding="async">
<button type="button" class="odl-fav" aria-label="Save Saffrine Greens" aria-pressed="false"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg></span></button>
</div>
<div class="odl-info">
<div class="odl-row"><h4><a href="#">Saffrine Greens</a></h4><span class="odl-rating"><svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.010a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/></svg><span class="odl-sr">Rated </span>4.6<span class="odl-sr"> out of 5,</span> <span class="odl-count">(860)</span></span></div>
<p class="odl-cuisine">Salads · Grain bowls · $$</p>
<p class="odl-meta"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>20–30 min</span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18.5" cy="17.5" r="3.5"/><circle cx="5.5" cy="17.5" r="3.5"/><circle cx="15" cy="5" r="1"/><path d="M12 17.5V14l-3-3 4-3 2 3h2"/></svg>$1.99 delivery</span></p>
<p class="odl-promo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/></svg>Buy 1, get 1 on bowls</p>
</div>
</li>
<li>
<div class="odl-cover">
<img src="https://images.unsplash.com/photo-1569718212165-3a8278d5f624?w=720&h=450&fit=crop&auto=format&q=70" alt="A bowl of ramen with a soft egg" width="360" height="225" loading="lazy" decoding="async">
<button type="button" class="odl-fav" aria-label="Save Hoshiyagi Ramen Bar" aria-pressed="false"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg></span></button>
</div>
<div class="odl-info">
<div class="odl-row"><h4><a href="#">Hoshiyagi Ramen Bar</a></h4><span class="odl-rating"><svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.010a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/></svg><span class="odl-sr">Rated </span>4.9<span class="odl-sr"> out of 5,</span> <span class="odl-count">(2.4k)</span></span></div>
<p class="odl-cuisine">Japanese · Ramen · $$$</p>
<p class="odl-meta"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>30–40 min</span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18.5" cy="17.5" r="3.5"/><circle cx="5.5" cy="17.5" r="3.5"/><circle cx="15" cy="5" r="1"/><path d="M12 17.5V14l-3-3 4-3 2 3h2"/></svg>$2.49 delivery</span></p>
<p class="odl-promo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>New on the app</p>
</div>
</li>
</ul>
</div>
</section>
<script>
document.querySelectorAll(".odl-restaurant-card").forEach((root) => {
if (root.dataset.odlReady) return; // wire once, even if the file is pasted twice
root.dataset.odlReady = "1";
root.querySelectorAll(".odl-fav").forEach((b) => b.addEventListener("click", () => {
const on = b.getAttribute("aria-pressed") !== "true"; b.setAttribute("aria-pressed", String(on));
}));
root.querySelectorAll("h4 a").forEach((a) => a.addEventListener("click", (e) => { if (a.getAttribute("href") === "#") e.preventDefault(); }));
});
</script>
The top of a product page: a swipeable photo gallery with dots on a phone and thumbnails from tablet up, name, sale price, rating, colour swatches, size chips with one sold out, Add to bag and save.
A holiday rental or hotel listing: a swipeable photo carousel with dots, a favourite heart, the place, host and dates, the rating, and the price per night with the total.
Three plans as cards you select, with a monthly or yearly switch that changes the prices and one plan marked as the common choice.