diff --git a/website/src/components/card-grid.js b/website/src/components/card-grid.js
index 0281af48..cb1d4f73 100644
--- a/website/src/components/card-grid.js
+++ b/website/src/components/card-grid.js
@@ -90,16 +90,21 @@ function renderCategorySection(category, allAnchors) {
- ${categoryAnchors.map((anchor) => renderAnchorCard(anchor, color)).join('')}
+ ${categoryAnchors.map((anchor) => renderAnchorCard(anchor, color, category.id)).join('')}
`
}
/**
- * Render a single anchor card
+ * Render a single anchor card.
+ *
+ * The optional `categoryId` argument is used to namespace the heading id
+ * used by `aria-labelledby`, since the same anchor may appear in multiple
+ * category sections (anchors can belong to more than one category) and
+ * the DOM must not contain duplicate ids.
*/
-function renderAnchorCard(anchor, categoryColor) {
+function renderAnchorCard(anchor, categoryColor, categoryId) {
const isUmbrella = anchor.subAnchors && anchor.subAnchors.length > 0
const umbrellaClass = isUmbrella ? ' anchor-card-umbrella' : ''
const rolesCount = anchor.roles ? anchor.roles.length : 0
@@ -110,19 +115,21 @@ function renderAnchorCard(anchor, categoryColor) {
const editTitle = i18n.t('card.edit')
const copyLinkTitle = i18n.t('card.copyLink')
const safeId = escapeHtml(anchor.id)
+ const safeCategoryId = escapeHtml(categoryId || 'uncat')
+ const cardTitleId = `anchor-card-title-${safeCategoryId}-${safeId}`
return `
-