BannerImage

apps/www/src/sections/BannerImage.astro · 80 lines · group Dividers

BannerImage — a full-width band of one photo, or of several photos cross-fading into each other. Use for: a visual pause between sections — a workshop, a landscape, a product line — with no text on it. Not for: a first screen with a title (<HeroCover>); a photo with a caption (<Gallery>). Children: none. Pictures: `imageName` is one file name in images/ or an array of them; use WIDE photos (the default band is about 4.4:1, `aspect` changes it as a Tailwind aspect class such as "aspect-[3/1]"). Several photos rotate every `interval` seconds, and stay still for visitors who prefer reduced motion.

In the fleet

Used on 1 site built with this engine.

Samples 1

sample
MDX of this sample
<BannerImage imageName={["photo-wide-01.jpg", "photo-wide-02.jpg", "photo-wide-03.jpg"]} />

Props

NameTypeRequiredDefaultDescription
imageNamestring | string[]yesone file name in images/, or several to cross-fade between
altstring''alt text, only when the photo carries meaning; a decorative band stays silent
intervalnumber6seconds each photo stays up; ignored for a single photo
aspectstring'aspect-[3840/880]'the band's proportions, as a Tailwind aspect class, e.g. "aspect-[3/1]"

Source apps/www/src/sections/BannerImage.astro @ gitt.one

---
/**
 * BannerImage — a full-width band of one photo, or of several photos
 *   cross-fading into each other.
 *
 * Use for: a visual pause between sections — a workshop, a landscape, a
 *   product line — with no text on it.
 * Not for: a first screen with a title (<HeroCover>); a photo with a caption
 *   (<Gallery>).
 * Children: none.
 * Pictures: `imageName` is one file name in images/ or an array of them; use
 *   WIDE photos (the default band is about 4.4:1, `aspect` changes it as a
 *   Tailwind aspect class such as "aspect-[3/1]"). Several photos rotate every
 *   `interval` seconds, and stay still for visitors who prefer reduced motion.
 */
import { Image } from 'astro:assets';
import { resolveImage } from '../lib/images';

interface Props {
  /** one file name in images/, or several to cross-fade between */
  imageName: string | string[];
  /** alt text, only when the photo carries meaning; a decorative band stays silent */
  alt?: string;
  /** seconds each photo stays up; ignored for a single photo */
  interval?: number;
  /** the band's proportions, as a Tailwind aspect class, e.g. "aspect-[3/1]" */
  aspect?: string;
}

const { imageName, alt = '', interval = 6, aspect = 'aspect-[3840/880]' } = Astro.props;
const names = Array.isArray(imageName) ? imageName : [imageName];
const images = await Promise.all(names.map((name) => resolveImage(name)));
---

<div
  class:list={[
    // the band follows the photo's own proportions, so nothing is cut off the
    // top or bottom; the bounds only bite on very narrow or very wide screens
    'relative max-h-[36rem] min-h-64 w-full overflow-hidden',
    aspect,
  ]}
  data-banner={images.length > 1 ? String(interval) : undefined}
>
  {
    images.map((image, i) => (
      <Image
        src={image}
        alt={i === 0 ? alt : ''}
        widths={[800, 1440, 1920, 2560]}
        loading={i === 0 ? 'eager' : 'lazy'}
        fetchpriority={i === 0 ? 'high' : 'auto'}
        class:list={[
          'absolute inset-0 h-full w-full object-cover object-center transition-opacity duration-1000',
          i === 0 ? 'opacity-100' : 'opacity-0',
        ]}
      />
    ))
  }
</div>

<script>
  // cross-fade the banner photos, unless the visitor asked for less motion.
  // Waits for load: nothing should be moving while the page is still arriving.
  window.addEventListener('load', () => {
    document.querySelectorAll<HTMLElement>('[data-banner]').forEach((banner) => {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      const slides = Array.from(banner.querySelectorAll('img'));
      let current = 0;
      setInterval(
        () => {
          slides[current].classList.replace('opacity-100', 'opacity-0');
          current = (current + 1) % slides.length;
          slides[current].classList.replace('opacity-0', 'opacity-100');
        },
        Number(banner.dataset.banner) * 1000,
      );
    });
  });
</script>

Source links point to engine-astro@7b686388. Samples are demo content: the components are the real ones, the copy and the pictures are made up.