EventCard
ImplementedEPASS card organism — a square image or branded no-image placeholder (title monogram + optional localized category caption), with date and price chips, sold-out scrim, and a solid-background text block. The whole card is one link; no CTA button. PRICE CONTRACT (ADR-067 §2): `priceLabel` is used verbatim; without it, `priceFrom === 0` means unknown and is not announced as free. Category is visible only in the image-less placeholder and included in the card's accessible name.
components/organisms/EventCard/EventCard.tsxPublic-facing event card with image on top, text in a solid section below (WCAG-readable). Locale-aware price label. Branded teal-to-blue gradient placeholder for events without an image. D-HOME-P4a 2026-08-11.
EventCard
Storefront organism (Family 7 — Tier 3)
A public-facing event card for listing pages (homepage, /events). Renders an aspect-square (1:1) image (or branded gradient placeholder) with date badge and price chip overlaid, followed by a solid-bg text section with title and venue. The slot is 1:1 because the card is fed profile_image_url, authored at 400×400 (EVENT-UPLOAD-01 2026-08-14). The entire card is a single <a> with a descriptive aria-label.
This component supports light and dark mode.
Usage
import { EventCard } from '@epasslive/design-system'
// With image
<EventCard
id="evt_1"
title="Summer Jazz Night"
image={{ url: "/banners/jazz.jpg", alt: "Saxophone at sunset" }}
date={{ day: "12", month: "AUG", year: "2026", iso: "2026-08-12T00:00:00Z" }}
venue={{ name: "Club Soda", city: "Montréal" }}
priceFrom={25}
currency="CAD"
priceLabel="From $25.00" // or "À partir de 25,00 $" for FR
href="/events/evt_1"
/>
// Without image (branded gradient placeholder)
<EventCard
id="evt_2"
title="Comedy Night"
image={{ url: "", alt: "Comedy show" }}
date={{ day: "15", month: "AUG", year: "2026", iso: "2026-08-15T00:00:00Z" }}
venue={{ name: "Théâtre Saint-Denis", city: "Montréal" }}
priceFrom={0}
currency="CAD"
href="/events/evt_2"
/>
// Sold out
<EventCard {...props} soldOut />
// With badges
<EventCard {...props} badges={[{ label: "Hot", variant: "hot" }]} />
// French — i18n labels bag (chip "À partir de {price}", label "Billets épuisés",
// screen-reader fragments "complet" / "gratuit" / "image à venir")
import { EVENTCARD_LABELS_FR } from '@epasslive/design-system'
<EventCard {...props} labels={{ ...EVENTCARD_LABELS_FR }} />Visual structure (D-HOME-P4a 2026-08-11)
┌─────────────────────────┐ │ [IMAGE] │ aspect-square (1:1), object-cover │ [Date] [Price] │ badges top-left/top-right (solid bg + shadow-lg) │ [Sold out scrim] │ only when soldOut=true └─────────────────────────┘ ┌─────────────────────────┐ │ Title │ solid bg-ep-surface-card, WCAG AA guaranteed │ Venue · City │ └─────────────────────────┘ No-image cards: teal→blue gradient replaces [IMAGE] section. Image slot is 1:1 (profile_image_url authored 400×400) — EVENT-UPLOAD-01 2026-08-14.
Accessibility
The entire card is a single <a> element with a descriptive aria-label:"Event title, AUG 12 2026, Venue City, from $25.00". The label adapts to state: ", sold out" or ", free" when applicable. Decorative elements (date badge, price chip, badges) have aria-hidden="true".
Text is always rendered on a solid bg-ep-surface-card background (D-HOME-P4a 2026-08-11), guaranteeing WCAG AA contrast regardless of image content. Previously, text floated on the image via an undefined ep-glass-card class, causing readability issues.
i18n
The priceLabel prop allows locale-aware price formatting. The adapter (eventGridAdapter.ts) pre-formats this:
- EN:
From $25.00(viaIntl.NumberFormat("en-CA", ...)) - FR:
À partir de 25,00 $(viaIntl.NumberFormat("fr-CA", ...))
Price-chip precedence (highest first): priceLabel → labels.fromPriceTemplate → English default "From {price}". When none is provided, the component falls back to English "From {price}". For French, spread the FR bag — labels={{ ...EVENTCARD_LABELS_FR }} — which also localizes the sold-out label (Billets épuisés) and the screen-reader fragments (complet / gratuit / image à venir).
Used by
EventGrid— renders a grid of EventCardsFeaturedEvents— wraps EventGrid for the homepage- Homepage (
/) and Events listing (/events)
Props
id*stringUnique event identifier. Used for data-event-id and React key.
title*stringEvent name. Rendered as <h3> with line-clamp-2.
image*{ url: string | null; alt: string }Profile image (1:1 slot). When url is empty or null, renders a branded teal-to-blue gradient placeholder instead of an <img>.
date*{ day: string; month: string; year: string; iso: string }Date parts for the badge (top-left). day and month are displayed; iso is used in aria-label.
venue*{ name: string; city: string }Venue name and city. Rendered as "Venue · City" or just "Venue" if city is empty.
priceFrom*numberMinimum ticket price. When > 0 and not sold out, shows a price chip top-right. When 0, the chip is hidden (unless priceLabel is provided) and aria-label says "free".
currency*stringISO currency code (e.g. "CAD"). Used for Intl.NumberFormat fallback when priceLabel is not provided.
priceLabelstringPre-formatted, locale-aware price label (e.g. "À partir de 25,00 $"). Highest precedence — overrides labels.fromPriceTemplate and the English "From {price}" default. D-HOME-P4c 2026-08-11.
labelsEventCardLabelsi18n labels bag — EN defaults when omitted. Spread the FR bundle: labels={{ ...EVENTCARD_LABELS_FR }}. Keys: fromPriceTemplate ("From {price}"), soldOutLabel ("Sold out"), soldOutSummary ("sold out"), freeSummary ("free"), noImageSummary ("no image available").
href*stringLink to the event detail page. The entire card is wrapped in <a href>.
soldOutbooleanfalseWhen true: shows a "Sold out" scrim over the image, hides the price chip, and aria-label includes "sold out".
badges{ label: string; variant: "new" | "hot" | "last-chance" }[]Optional promotional badges stacked below the price chip.
Props
EventCard
idstring—Unique event id (rendered as data-event-id on the link).
titlestring—Event name (line-clamp-2).
image{ url: string | null; alt: string }—1:1 profile image; null/empty url renders the gradient placeholder (aspect-square slot — not the wide banner).
date{ day: string; month: string; year: string; iso: string }—Pre-formatted date parts for the top-left chip; iso is carried for data/SEO tooling.
venue{ name: string; city: string }—Venue line — 'name · city' (city hidden when empty).
priceFromnumber—Minimum price; 0 is ambiguous (free OR unknown) — with no priceLabel the card announces NO price and shows no price chip. Pass priceLabel to state the price.
currencystring—ISO currency code for the Intl.NumberFormat('en') fallback.
priceLabelstring—Pre-formatted locale-aware price (e.g. 'À partir de 25,00 $') — highest precedence, overrides labels.fromPriceTemplate.
hrefstring—Event detail link — the card's single <a> target.
soldOutbooleanfalseTop-right sold-out pill with labels.soldOutLabel; hides the price chip.
badgesEventCardBadge[][]{ label, variant: 'new' | 'hot' | 'last-chance' } promotional chips (aria-hidden).
labelsEventCardLabelsEN defaultsi18n bag — fromPriceTemplate, soldOutLabel, soldOutSummary, freeSummary, noImageSummary. FR: spread EVENTCARD_LABELS_FR. ADR-067 §2: freeSummary stays in the bag for API compatibility but is no longer emitted — announce a free event with priceLabel.
categorystring | nullnullAlready-localized event category. Shown as a readable caption on the no-image placeholder and included in the link's accessible name; ignored when an image is present.
<EventCard />Accessibility
ARIA
1 rulea11y
2 rulesUsage
Import
import { EventCard } from "@epasslive/design-system"Example 1
<EventCard />Example 2
<EventCard {...event} labels={{ ...EVENTCARD_LABELS_FR }} />Example 3
<EventCard title={event.name} category={event.category_name_fr} image={{ url: null, alt: '' }} />Anti-patterns
— Do not fetch data directly in organisms
— Avoid tight coupling to specific data sources
— Do not expect a CTA button — the entire card is the single link target
— Do not pass the wide banner image — the slot is aspect-square (profile image)