Ticketing — Event Card
A reusable event ticketing card pattern rendered as a responsive grid of six distinctive variants. Each card pairs a photographic gradient hero with a floating date badge, category tag, venue line, and a prominent price-from figure. Interactive states cover a save heart toggle with a satisfying pop, hover lift, low-stock and sold-out badges, plus disabled checkout for unavailable shows. Keyboard friendly with a lightweight toast helper and full WCAG-minded contrast.
MCP
Code
:root {
--brand: #7c3aed;
--brand-d: #6d28d9;
--ink: #0e0e16;
--ink-2: #3a3a4d;
--muted: #6c6c80;
--bg: #f5f4f9;
--surface: #ffffff;
--line: rgba(14, 14, 22, 0.1);
--ok: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--accent: #ff3d81;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(14, 14, 22, 0.06), 0 6px 16px rgba(14, 14, 22, 0.08);
--sh-2: 0 18px 40px rgba(14, 14, 22, 0.18);
--c-music: #7c3aed;
--c-arts: #ff3d81;
--c-sport: #0ea5e9;
--c-talk: #d97706;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background:
radial-gradient(1100px 520px at 12% -10%, rgba(124, 58, 237, 0.12), transparent 60%),
radial-gradient(900px 480px at 100% 0%, rgba(255, 61, 129, 0.1), transparent 55%),
var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.wrap { width: min(1120px, 100% - 40px); margin-inline: auto; }
/* ---------- header ---------- */
.page-head { padding: 52px 0 22px; }
.kicker {
margin: 0 0 8px;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--brand-d);
}
.page-head h1 {
margin: 0;
font-size: clamp(30px, 5vw, 46px);
font-weight: 800;
letter-spacing: -0.02em;
}
.page-head .sub {
margin: 10px 0 0;
max-width: 56ch;
color: var(--muted);
font-size: 15px;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
margin-top: 18px;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
}
.dot { width: 11px; height: 11px; border-radius: 50%; display: inline-block; }
.dot-music { background: var(--c-music); }
.dot-arts { background: var(--c-arts); }
.dot-sport { background: var(--c-sport); }
.dot-talk { background: var(--c-talk); }
/* ---------- grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 22px;
padding: 12px 0 64px;
}
/* ---------- card ---------- */
.card {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-1);
transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
outline: none;
}
.card:hover,
.card:focus-visible {
transform: translateY(-6px);
box-shadow: var(--sh-2);
border-color: rgba(124, 58, 237, 0.4);
}
.card:focus-visible { box-shadow: var(--sh-2), 0 0 0 3px rgba(124, 58, 237, 0.35); }
/* ---------- media ---------- */
.media {
position: relative;
aspect-ratio: 16 / 10;
background-size: cover;
background-position: center;
}
.media::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 40%, rgba(8, 8, 16, 0.42));
}
.media-music { background-image: linear-gradient(135deg, #7c3aed, #ff3d81 130%); }
.media-music2 { background-image: linear-gradient(135deg, #4338ca, #06b6d4); }
.media-arts { background-image: linear-gradient(135deg, #ff3d81, #f97316); }
.media-arts2 { background-image: linear-gradient(135deg, #db2777, #7c3aed); }
.media-sport { background-image: linear-gradient(135deg, #0ea5e9, #1e293b); }
.media-talk { background-image: linear-gradient(135deg, #d97706, #be123c); }
.date-badge {
position: absolute;
top: 12px;
left: 12px;
z-index: 2;
display: grid;
justify-items: center;
min-width: 48px;
padding: 7px 9px;
background: var(--surface);
border-radius: var(--r-sm);
box-shadow: 0 4px 12px rgba(8, 8, 16, 0.25);
line-height: 1;
}
.date-badge b { font-size: 19px; font-weight: 800; color: var(--ink); }
.date-badge span {
margin-top: 3px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--brand-d);
}
.badge {
position: absolute;
bottom: 12px;
left: 12px;
z-index: 2;
padding: 5px 11px;
font-size: 11.5px;
font-weight: 700;
letter-spacing: 0.02em;
border-radius: 999px;
color: #fff;
}
.badge-low { background: var(--warn); }
.badge-sold { background: var(--danger); }
/* ---------- save heart ---------- */
.save {
position: absolute;
top: 12px;
right: 12px;
z-index: 2;
width: 40px;
height: 40px;
display: grid;
place-items: center;
padding: 0;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 4px 12px rgba(8, 8, 16, 0.25);
cursor: pointer;
transition: transform 0.16s ease, background 0.16s ease;
}
.save svg { width: 20px; height: 20px; fill: none; stroke: var(--ink-2); stroke-width: 1.8; transition: fill 0.18s ease, stroke 0.18s ease; }
.save:hover { transform: scale(1.08); }
.save:active { transform: scale(0.92); }
.save:focus-visible { outline: 3px solid rgba(124, 58, 237, 0.45); outline-offset: 2px; }
.save[aria-pressed="true"] svg { fill: var(--accent); stroke: var(--accent); }
.save.pulse { animation: pop 0.32s ease; }
@keyframes pop {
0% { transform: scale(1); }
45% { transform: scale(1.28); }
100% { transform: scale(1); }
}
/* ---------- body ---------- */
.body { padding: 16px 16px 18px; }
.tag {
display: inline-block;
padding: 3px 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
border-radius: 999px;
}
.tag-music { color: var(--c-music); background: rgba(124, 58, 237, 0.12); }
.tag-arts { color: var(--c-arts); background: rgba(255, 61, 129, 0.12); }
.tag-sport { color: var(--c-sport); background: rgba(14, 165, 233, 0.14); }
.tag-talk { color: var(--c-talk); background: rgba(217, 119, 6, 0.14); }
.title {
margin: 10px 0 4px;
font-size: 19px;
font-weight: 800;
letter-spacing: -0.01em;
line-height: 1.25;
}
.venue { margin: 0; font-size: 13.5px; color: var(--muted); }
.foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px dashed var(--line);
}
.price { font-size: 20px; font-weight: 800; color: var(--ink); }
.price small { display: block; font-size: 11px; font-weight: 600; color: var(--muted); letter-spacing: 0.04em; }
.cta {
border: none;
padding: 10px 16px;
border-radius: 999px;
background: var(--brand);
color: #fff;
font-family: inherit;
font-size: 13.5px;
font-weight: 700;
cursor: pointer;
transition: background 0.16s ease, transform 0.12s ease;
}
.cta:hover { background: var(--brand-d); }
.cta:active { transform: translateY(1px); }
.cta:focus-visible { outline: 3px solid rgba(124, 58, 237, 0.45); outline-offset: 2px; }
.cta:disabled { background: #e7e6ee; color: var(--muted); cursor: not-allowed; }
/* ---------- sold out ---------- */
.card.is-soldout .media { filter: grayscale(0.55) brightness(0.92); }
.card.is-soldout .title,
.card.is-soldout .price { color: var(--ink-2); }
.card.is-soldout:hover,
.card.is-soldout:focus-visible { transform: translateY(-2px); }
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
z-index: 50;
padding: 11px 18px;
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 600;
border-radius: 999px;
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
@media (max-width: 520px) {
.page-head { padding: 34px 0 14px; }
.wrap { width: min(1120px, 100% - 28px); }
.grid { grid-template-columns: 1fr; gap: 18px; }
.title { font-size: 18px; }
}
@media (prefers-reduced-motion: reduce) {
* { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}(function () {
"use strict";
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
function cardTitle(card) {
var t = card.querySelector(".title");
return t ? t.textContent.trim() : "event";
}
// ----- save / heart toggle -----
document.querySelectorAll(".save").forEach(function (btn) {
btn.addEventListener("click", function (e) {
e.stopPropagation();
var pressed = btn.getAttribute("aria-pressed") === "true";
btn.setAttribute("aria-pressed", pressed ? "false" : "true");
btn.classList.remove("pulse");
// reflow to restart animation
void btn.offsetWidth;
if (!pressed) btn.classList.add("pulse");
var name = cardTitle(btn.closest(".card"));
toast(pressed ? "Removed " + name + " from saved" : "Saved " + name);
});
});
// ----- get tickets cta -----
document.querySelectorAll(".cta").forEach(function (btn) {
btn.addEventListener("click", function (e) {
e.stopPropagation();
if (btn.disabled) return;
var name = cardTitle(btn.closest(".card"));
toast("Opening checkout for " + name + "…");
});
});
// ----- card activation (Enter / Space on focused card) -----
document.querySelectorAll(".card").forEach(function (card) {
card.addEventListener("keydown", function (e) {
if (e.key !== "Enter" && e.key !== " ") return;
// don't hijack when focus is on an inner control
if (e.target !== card) return;
e.preventDefault();
if (card.classList.contains("is-soldout")) {
toast(cardTitle(card) + " is sold out");
} else {
toast("Opening checkout for " + cardTitle(card) + "…");
}
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ticketing — Event Card</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="page-head">
<div class="wrap">
<p class="kicker">On sale now</p>
<h1>Upcoming events</h1>
<p class="sub">Tap the heart to save an event. Cards lift on hover; sold-out shows are dimmed and locked.</p>
<div class="legend" aria-hidden="true">
<span class="legend-item"><i class="dot dot-music"></i>Music</span>
<span class="legend-item"><i class="dot dot-arts"></i>Arts</span>
<span class="legend-item"><i class="dot dot-sport"></i>Sport</span>
<span class="legend-item"><i class="dot dot-talk"></i>Talks</span>
</div>
</div>
</header>
<main class="wrap">
<section class="grid" aria-label="Event cards" id="grid">
<!-- Card 1 — Music, low stock -->
<article class="card" data-category="music" tabindex="0" aria-label="Neon Tide Tour, The Lumen Hall">
<div class="media media-music">
<span class="date-badge"><b>14</b><span>JUN</span></span>
<span class="badge badge-low">Only 12 left</span>
<button class="save" type="button" aria-pressed="false" aria-label="Save Neon Tide Tour">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.3C.4 8.3 1.7 4.7 5.2 4.1 7.4 3.7 9.4 4.9 12 7.6c2.6-2.7 4.6-3.9 6.8-3.5 3.5.6 4.8 4.2 3.2 7.6C19.5 16.4 12 21 12 21z"/></svg>
</button>
</div>
<div class="body">
<span class="tag tag-music">Music</span>
<h2 class="title">Neon Tide Tour</h2>
<p class="venue">The Lumen Hall · Brighton</p>
<div class="foot">
<span class="price"><small>from</small> £38</span>
<button class="cta" type="button">Get tickets</button>
</div>
</div>
</article>
<!-- Card 2 — Arts -->
<article class="card" data-category="arts" tabindex="0" aria-label="Veil & Velvet, Arden Playhouse">
<div class="media media-arts">
<span class="date-badge"><b>22</b><span>JUN</span></span>
<button class="save" type="button" aria-pressed="false" aria-label="Save Veil and Velvet">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.3C.4 8.3 1.7 4.7 5.2 4.1 7.4 3.7 9.4 4.9 12 7.6c2.6-2.7 4.6-3.9 6.8-3.5 3.5.6 4.8 4.2 3.2 7.6C19.5 16.4 12 21 12 21z"/></svg>
</button>
</div>
<div class="body">
<span class="tag tag-arts">Arts</span>
<h2 class="title">Veil & Velvet</h2>
<p class="venue">Arden Playhouse · Leeds</p>
<div class="foot">
<span class="price"><small>from</small> £24</span>
<button class="cta" type="button">Get tickets</button>
</div>
</div>
</article>
<!-- Card 3 — Sport, sold out -->
<article class="card is-soldout" data-category="sport" tabindex="0" aria-label="City Derby Finals, Riverside Arena, sold out">
<div class="media media-sport">
<span class="date-badge"><b>28</b><span>JUN</span></span>
<span class="badge badge-sold">Sold out</span>
<button class="save" type="button" aria-pressed="false" aria-label="Save City Derby Finals">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.3C.4 8.3 1.7 4.7 5.2 4.1 7.4 3.7 9.4 4.9 12 7.6c2.6-2.7 4.6-3.9 6.8-3.5 3.5.6 4.8 4.2 3.2 7.6C19.5 16.4 12 21 12 21z"/></svg>
</button>
</div>
<div class="body">
<span class="tag tag-sport">Sport</span>
<h2 class="title">City Derby Finals</h2>
<p class="venue">Riverside Arena · Manchester</p>
<div class="foot">
<span class="price"><small>from</small> £52</span>
<button class="cta" type="button" disabled>Sold out</button>
</div>
</div>
</article>
<!-- Card 4 — Talks -->
<article class="card" data-category="talk" tabindex="0" aria-label="Futures Unwritten, The Quay Centre">
<div class="media media-talk">
<span class="date-badge"><b>03</b><span>JUL</span></span>
<button class="save" type="button" aria-pressed="false" aria-label="Save Futures Unwritten">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.3C.4 8.3 1.7 4.7 5.2 4.1 7.4 3.7 9.4 4.9 12 7.6c2.6-2.7 4.6-3.9 6.8-3.5 3.5.6 4.8 4.2 3.2 7.6C19.5 16.4 12 21 12 21z"/></svg>
</button>
</div>
<div class="body">
<span class="tag tag-talk">Talks</span>
<h2 class="title">Futures Unwritten</h2>
<p class="venue">The Quay Centre · Bristol</p>
<div class="foot">
<span class="price"><small>from</small> £15</span>
<button class="cta" type="button">Get tickets</button>
</div>
</div>
</article>
<!-- Card 5 — Music, low stock -->
<article class="card" data-category="music" tabindex="0" aria-label="Goldlight Sessions, Aurora Rooms">
<div class="media media-music2">
<span class="date-badge"><b>09</b><span>JUL</span></span>
<span class="badge badge-low">Only 7 left</span>
<button class="save" type="button" aria-pressed="false" aria-label="Save Goldlight Sessions">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.3C.4 8.3 1.7 4.7 5.2 4.1 7.4 3.7 9.4 4.9 12 7.6c2.6-2.7 4.6-3.9 6.8-3.5 3.5.6 4.8 4.2 3.2 7.6C19.5 16.4 12 21 12 21z"/></svg>
</button>
</div>
<div class="body">
<span class="tag tag-music">Music</span>
<h2 class="title">Goldlight Sessions</h2>
<p class="venue">Aurora Rooms · Glasgow</p>
<div class="foot">
<span class="price"><small>from</small> £29</span>
<button class="cta" type="button">Get tickets</button>
</div>
</div>
</article>
<!-- Card 6 — Arts -->
<article class="card" data-category="arts" tabindex="0" aria-label="The Paper Garden, Halcyon Studio">
<div class="media media-arts2">
<span class="date-badge"><b>16</b><span>JUL</span></span>
<button class="save" type="button" aria-pressed="false" aria-label="Save The Paper Garden">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.3C.4 8.3 1.7 4.7 5.2 4.1 7.4 3.7 9.4 4.9 12 7.6c2.6-2.7 4.6-3.9 6.8-3.5 3.5.6 4.8 4.2 3.2 7.6C19.5 16.4 12 21 12 21z"/></svg>
</button>
</div>
<div class="body">
<span class="tag tag-arts">Arts</span>
<h2 class="title">The Paper Garden</h2>
<p class="venue">Halcyon Studio · Cardiff</p>
<div class="foot">
<span class="price"><small>from</small> £19</span>
<button class="cta" type="button">Get tickets</button>
</div>
</div>
</article>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Event Card
A drop-in event card built for a ticketing listing. Every card leads with a photographic gradient hero overlaid with a floating calendar date badge, then steps down through a colour-coded category tag, the event title, and the venue line. A dashed perforation separates the body from the footer, where a bold price-from figure sits opposite a pill-shaped get-tickets call to action — echoing the torn-stub motif of a real ticket.
The six variants in the grid demonstrate the full state matrix: standard availability, two low-stock cards with amber “Only N left” badges, and a sold-out show that greys its hero, locks its checkout button, and softens its hover lift. A circular save heart in the top corner toggles between outline and filled accent pink, firing a quick pop animation and a confirmation toast each time.
Interactions are vanilla JavaScript with no dependencies. The heart and CTA stop event propagation so inner controls never clash, while focused cards respond to Enter and Space for keyboard users. The layout reflows from a multi-column auto-fill grid down to a single column at narrow widths, staying legible to roughly 360px.
Illustrative UI only — fictional events, not a real ticketing service.