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.
Pre-mortem
Imagine the project has already failed, then work back to the risks people privately worry about but rarely say out loud.
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. |
| Has a sketch | A non-empty illustration adds a band across the top of the card, the same one the tips cards use. Exercises get it from exercises.illustration_path; without one the card is unchanged. |
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 |
illustration | string | Web path of a sketch to show across the top; empty for none |
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-illustration | Optional sketch band across the top of the card |
.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) |