Listing Card
The library's content card — one workshop, exercise, or icebreaker, ready to open.
When to Use
Variants
There's one card. What varies is the record it's given — the category icon and colour, the duration, and whether the item is already in a collection or has comments. All of it comes from prepareCardData().
Design Sprint
A 5-day process for answering critical business questions through design, prototyping, and testing ideas with customers.
Crazy Eights
A fast sketching exercise that challenges people to sketch 8 distinct ideas in 8 minutes.
Two Truths and a Lie
A classic icebreaker where each person shares two true statements and one false one. The group guesses the lie.
categories table per record. They're data, not phase tokens — which is why they aren't red and shouldn't be replaced with one.
States
| State | Behavior |
|---|---|
| Default | The whole card is an <a> to detail_url, with the action buttons layered in the footer. |
| Hover | Card lifts; action buttons brighten. |
| Focus | Native focus ring on the card link and on each action button. |
| In a collection | in_collection: true renders the favourite button in its active state. |
| Has comments | A count badge appears in the meta row when comment_count is above zero, with a title pluralised for one comment. |
| Has notes | The notes button gains .has-note and a red note_add icon — applied by JS after load, not server-rendered, so it appears a moment after the card. |
| Long overview | Truncated to roughly 150 characters in the card. The full text lives on the detail page. |
Real-World Usage
The exercises listing. prepareCardData() does the shaping — category lookup, duration formatting, truncation — so pages don't build the array by hand.
<?php
require_once 'includes/listing-helpers.php';
?>
<div class="listing-grid">
<?php foreach ($exercises as $item): ?>
<?= ws_listing_card(prepareCardData($item, $pageData)) ?>
<?php endforeach; ?>
</div>
<?php if (!$exercises): ?>
<?= ws_empty_state('Nothing matches those filters', [
'icon' => 'filter_alt_off', 'action' => 'Clear filters', 'actionHref' => '?',
]) ?>
<?php endif; ?>Including the partial directly still works and produces the same markup — the helper just wraps it:
<?php $card = prepareCardData($item, $pageData); include 'includes/partials/listing-card.php'; ?>
Options
ws_listing_card() takes one positional array — the output of prepareCardData(). These are the keys it reads.
| Key | Type | Purpose |
|---|---|---|
id | string | Record ID, used by the action buttons |
title | string | Card heading |
detail_url | string | Where the card links. Effectively required — without it the card links nowhere. |
overview | string | Description, truncated in the card |
category | string | Category name shown in the footer badge |
category_class | string | Slug used as a styling hook |
category_icon | string | Material icon for the header |
category_color | string | Hex from the record, tinting the icon holder |
duration | string | Formatted duration for display |
duration_raw | int | Minutes, for sorting and filtering |
type | string | workshop | exercise | icebreaker |
views | int | View count |
created_at | string | ISO date, for sorting |
in_collection | bool | Drives the favourite button's active state |
comment_count | int | Shows a badge when above zero |
Accessibility
| Concern | Behavior |
|---|---|
| Card link | The card is a real <a>, so it's focusable and announced as a link. Its accessible name is the title plus description — distinct per card in a link list. |
| Heading | The title is an <h3>, so a grid of cards is navigable by heading. Check that h3 suits your page outline. |
| Nested actions | Structural issue: the favourite, notes, and share buttons sit inside the card's own anchor. Interactive elements nested in a link are invalid HTML and produce unpredictable focus and click behaviour — the buttons rely on stopPropagation to avoid triggering navigation. Moving the actions outside the anchor would fix it. |
| Action labels | Favourite and notes carry aria-label; share has only a title, so its accessible name is weaker and inconsistent with its siblings. All three are also generic — "Add to favorites" repeated across 24 cards gives no way to tell which item is being favourited. Name the item: "Add Design Sprint to favorites". |
| Notes state | .has-note is applied by JS after load, and conveyed by icon and colour with no text equivalent. A screen-reader user can't tell an annotated card from a plain one. |
| Category colour | Decorative. The category name appears as text in the footer badge, so colour is reinforcement. |
| Truncation | The description is cut server-side, so assistive tech gets the same truncated text — no hidden content, which is correct. |
Tokens
Listing Card's styles live in css/listing-redesign.css, not components.css — a page rendering these cards must load that stylesheet too. It draws on the shared surface, border, radius, ink, and type scales; the only per-record colour is category_color, applied inline from the database rather than from a token.
CSS Classes
These use the .listing-card- prefix, not .ws- — the component predates the library's naming convention.
| Class | Purpose |
|---|---|
.card-wrapper | Outer grid cell |
.listing-card | The card anchor |
.listing-card-header | Icon, title, and meta row |
.listing-card-icon | Category icon holder, tinted from category_color |
.listing-card-title | The <h3> |
.listing-card-meta | Duration and comment count |
.listing-card-comments | Comment count badge |
.listing-card-description | Truncated overview |
.listing-card-footer | Category badge and actions |
.listing-card-actions | Action button group |
.listing-card-action | An action button |
.favorite-btn / .note-btn | Favourite and notes buttons |
.note-btn.has-note | Annotated state, applied by JS |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_listing_card() — a thin wrapper around the partial |
includes/partials/listing-card.php | The actual card markup |
includes/listing-helpers.php | prepareCardData() and the category utilities |
css/listing-redesign.css | All card styles (.listing-card-* rules) |