Card
{{ design.patterns.renderPattern('card', {}) | safe }}
Source (Nunjucks)
<article class="card">
<a href="{{ data.href }}">
<picture class="hover-anim"
data-static="{{ data.img.src }}.webp"
data-anim="{{ data.img.src }}-anim.webp">
<source type="image/webp" srcset="{{ data.img.src }}.webp">
<img
src="{{ data.img.src }}.webp"
alt="{{ data.img.alt }}"
loading="lazy"
draggable="false"/>
</picture>
{# The animation on hover effect for the card image #}
<script>
document.addEventListener('DOMContentLoaded', () => {
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('picture.hover-anim').forEach(pic => {
const img = pic.querySelector('img');
const source = pic.querySelector('source[type="image/webp"]');
if (!img) return;
const staticSrc = pic.dataset.static || img.currentSrc || img.src;
const animSrc =
pic.dataset.anim ||
staticSrc.replace(/(\.webp)(\?.*)?$/i, '-anim.webp$2'); // auto-derive
// If derived anim equals static, bail early
if (!animSrc || animSrc === staticSrc || reduceMotion) return;
// Preflight: only enable hover if the animation actually exists
const tester = new Image();
tester.onload = () => {
const swap = (url) => {
if (source) source.srcset = url; // drive <picture>
img.removeAttribute('srcset'); // ensure <picture> wins if any srcset lands
img.src = url; // mirror for fallback
};
// start/stop on hover (pointerenter/leave works for mouse + pen)
pic.addEventListener('pointerenter', () => swap(animSrc));
pic.addEventListener('pointerleave', () => swap(staticSrc));
// also reset on keyboard/tab focus changes just in case
pic.addEventListener('focusout', () => swap(staticSrc));
};
tester.onerror = () => {
// No anim available → do nothing, leave static image
// (You could console.debug here if you want)
};
tester.src = animSrc; // kicks off preflight
});
});
</script>
</a>
<div class="card__content flow">
<p class="card__runner">{{ data.runner }}</p>
<h2 class="card__title">{{ data.title }}</h2>
<p class="card__meta">{{ data.meta }}</p>
<p class="card__body">{{ data.description }}</p>
{% if data.alertText %}
<p class="card__alert">
<span>{{ data.alertText }}</span>
</p>
{% endif %}
<a class="button" href="{{ data.href }}">
<span>{{ data.buttonLabel }}</span>
{% include "icons/arrow.svg" %}
<span class="corner" aria-hidden="true"></span>
</a>
</div>
</article>
Variants
With alert text
{{ design.patterns.renderPattern('card/With alert text', {}) | safe }}
Source (Nunjucks)
<article class="card">
<a href="{{ data.href }}">
<picture class="hover-anim"
data-static="{{ data.img.src }}.webp"
data-anim="{{ data.img.src }}-anim.webp">
<source type="image/webp" srcset="{{ data.img.src }}.webp">
<img
src="{{ data.img.src }}.webp"
alt="{{ data.img.alt }}"
loading="lazy"
draggable="false"/>
</picture>
{# The animation on hover effect for the card image #}
<script>
document.addEventListener('DOMContentLoaded', () => {
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('picture.hover-anim').forEach(pic => {
const img = pic.querySelector('img');
const source = pic.querySelector('source[type="image/webp"]');
if (!img) return;
const staticSrc = pic.dataset.static || img.currentSrc || img.src;
const animSrc =
pic.dataset.anim ||
staticSrc.replace(/(\.webp)(\?.*)?$/i, '-anim.webp$2'); // auto-derive
// If derived anim equals static, bail early
if (!animSrc || animSrc === staticSrc || reduceMotion) return;
// Preflight: only enable hover if the animation actually exists
const tester = new Image();
tester.onload = () => {
const swap = (url) => {
if (source) source.srcset = url; // drive <picture>
img.removeAttribute('srcset'); // ensure <picture> wins if any srcset lands
img.src = url; // mirror for fallback
};
// start/stop on hover (pointerenter/leave works for mouse + pen)
pic.addEventListener('pointerenter', () => swap(animSrc));
pic.addEventListener('pointerleave', () => swap(staticSrc));
// also reset on keyboard/tab focus changes just in case
pic.addEventListener('focusout', () => swap(staticSrc));
};
tester.onerror = () => {
// No anim available → do nothing, leave static image
// (You could console.debug here if you want)
};
tester.src = animSrc; // kicks off preflight
});
});
</script>
</a>
<div class="card__content flow">
<p class="card__runner">{{ data.runner }}</p>
<h2 class="card__title">{{ data.title }}</h2>
<p class="card__meta">{{ data.meta }}</p>
<p class="card__body">{{ data.description }}</p>
{% if data.alertText %}
<p class="card__alert">
<span>{{ data.alertText }}</span>
</p>
{% endif %}
<a class="button" href="{{ data.href }}">
<span>{{ data.buttonLabel }}</span>
{% include "icons/arrow.svg" %}
<span class="corner" aria-hidden="true"></span>
</a>
</div>
</article>