Gonk Design: Pattern Library

View the website ←

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>

Gonk Design: Pattern Library

View the website ←