Canvas Design System
Main Site Tokens

Listing Card

The library's content card — one workshop, exercise, or icebreaker, ready to open.

When to Use

Use when: You're rendering library items in a grid or list — the exercises, workshops, and icebreakers pages. Each card links to a detail page and carries the favourite, notes, and share actions.
Don't use when: The content isn't a library record — use Card for a generic container or Feature Card for marketing grids. Inside the Planner, use Planner Card.

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

5 days 3

A 5-day process for answering critical business questions through design, prototyping, and testing ideas with customers.

Crazy Eights

8 min

A fast sketching exercise that challenges people to sketch 8 distinct ideas in 8 minutes.

Two Truths and a Lie

15 min 7

A classic icebreaker where each person shares two true statements and one false one. The group guesses the lie.

info Category colours come from the 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

StateBehavior
DefaultThe whole card is an <a> to detail_url, with the action buttons layered in the footer.
HoverCard lifts; action buttons brighten.
FocusNative focus ring on the card link and on each action button.
In a collectionin_collection: true renders the favourite button in its active state.
Has commentsA count badge appears in the meta row when comment_count is above zero, with a title pluralised for one comment.
Has notesThe 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 overviewTruncated 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.

KeyTypePurpose
idstringRecord ID, used by the action buttons
titlestringCard heading
detail_urlstringWhere the card links. Effectively required — without it the card links nowhere.
overviewstringDescription, truncated in the card
categorystringCategory name shown in the footer badge
category_classstringSlug used as a styling hook
category_iconstringMaterial icon for the header
category_colorstringHex from the record, tinting the icon holder
durationstringFormatted duration for display
duration_rawintMinutes, for sorting and filtering
typestringworkshop | exercise | icebreaker
viewsintView count
created_atstringISO date, for sorting
in_collectionboolDrives the favourite button's active state
comment_countintShows a badge when above zero

Accessibility

ConcernBehavior
Card linkThe 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.
HeadingThe title is an <h3>, so a grid of cards is navigable by heading. Check that h3 suits your page outline.
Nested actionsStructural 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 labelsFavourite 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 colourDecorative. The category name appears as text in the footer badge, so colour is reinforcement.
TruncationThe 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.

ClassPurpose
.card-wrapperOuter grid cell
.listing-cardThe card anchor
.listing-card-headerIcon, title, and meta row
.listing-card-iconCategory icon holder, tinted from category_color
.listing-card-titleThe <h3>
.listing-card-metaDuration and comment count
.listing-card-commentsComment count badge
.listing-card-descriptionTruncated overview
.listing-card-footerCategory badge and actions
.listing-card-actionsAction button group
.listing-card-actionAn action button
.favorite-btn / .note-btnFavourite and notes buttons
.note-btn.has-noteAnnotated state, applied by JS

Files

FilePurpose
includes/components/helpers.phpws_listing_card() — a thin wrapper around the partial
includes/partials/listing-card.phpThe actual card markup
includes/listing-helpers.phpprepareCardData() and the category utilities
css/listing-redesign.cssAll card styles (.listing-card-* rules)