Skip to content

03 — Image-Heavy Module

Create a module with significant image usage that requires careful optimization.
Examples: M04 Page Hero, M05 Hero Slider, M11 Image Gallery, M08 Card Grid (many cards).

Use alongside template 01 — Module Development for image-heavy modules.
The module should be created with template 01 first; this template adds image-specific guidance.


Read these files before executing the prompt:

docs/dev-frontend-guides/06_RESPONSIVE_IMAGES_PATTERN.md
docs/dev-frontend-guides/03_MODULE_DEVELOPMENT_LIFECYCLE.md
docs/dev-frontend-guides/05_BEM_SASS_THEMING.md
docs/PRD/13_Media_and_Image_Pipeline.md
docs/PRD/07_Modules_and_Templates.md
packages/ui/src/ # find ResponsiveImage component
packages/modules/src/m04-page-hero/ # existing image-heavy module example
packages/modules/src/registry.ts

Read the following context files first:
- docs/dev-frontend-guides/06_RESPONSIVE_IMAGES_PATTERN.md
- docs/dev-frontend-guides/03_MODULE_DEVELOPMENT_LIFECYCLE.md
- docs/dev-frontend-guides/05_BEM_SASS_THEMING.md
- docs/PRD/13_Media_and_Image_Pipeline.md
- docs/PRD/07_Modules_and_Templates.md
- packages/ui/src/ (find and read the ResponsiveImage component)
- packages/modules/src/m04-page-hero/ (all files in this directory)
- packages/modules/src/registry.ts
Now create a new image-heavy module with the following details:
**Module ID:** M{MODULE_NUMBER}
**Module Name:** {MODULE_NAME}
**Figma Reference:** {FIGMA_URL}
**Umbraco Content Type Alias:** {UMBRACO_ALIAS}
**Component Type:** {SERVER_OR_CLIENT — if client, also create .client.tsx}
**Description:**
{BRIEF_DESCRIPTION_OF_MODULE_PURPOSE}
**Image Contexts:**
For each distinct image usage in the module, specify:
| Context | Desktop Dimensions | Mobile Dimensions | Aspect Ratio | Loading | Position |
|---------|-------------------|-------------------|--------------|---------|----------|
| {IMAGE_CONTEXT_NAME} | {WIDTH}x{HEIGHT} | {WIDTH}x{HEIGHT} | {RATIO} | {eager/lazy} | {above-fold/below-fold} |
Example:
| Context | Desktop Dimensions | Mobile Dimensions | Aspect Ratio | Loading | Position |
|---------|-------------------|-------------------|--------------|---------|----------|
| Hero background | 1920x800 | 750x600 | 12:5 / 5:4 | eager | above-fold |
| Card thumbnail | 400x300 | 350x263 | 4:3 | lazy | below-fold |
**Focal Point Handling:**
{DESCRIBE_FOCAL_POINT_BEHAVIOR — e.g., respect CMS focal point for object-position, center-crop, etc.}
**Props:**
{LIST_EACH_PROP_WITH_TYPE_AND_DESCRIPTION — ensure every image has imageDesktop + imageMobile}
**UI Components to Use:**
ResponsiveImage, {OTHER_UI_COMPONENTS}
**Layout:**
{DESCRIBE_LAYOUT — grid, masonry, full-bleed, contained, etc.}
**LCP Considerations:**
{WHICH_IMAGE_IS_THE_LCP_CANDIDATE — typically the first/largest above-fold image}
Create all required files following the module lifecycle guide and the responsive images pattern guide.
Use the ResponsiveImage component for ALL images — never use raw <img> tags.
Key image requirements:
- Every image uses imageDesktop + imageMobile (never just one)
- LCP image uses priority={true} and loading="eager"
- Below-fold images use loading="lazy"
- Cloudflare image loader for URL transforms
- Focal point from CMS applied via object-position
- srcset with appropriate widths for each context
- sizes attribute matches actual rendered size at each breakpoint
- WebP/AVIF format negotiation via Cloudflare
- Desktop budget: max 500KB per image, Mobile budget: max 200KB per image
Follow all standard module conventions:
- BEM + SASS, CSS custom properties only
- Mapper with Omit<Props, 'siteKey' | 'locale'>
- Storybook stories, Vitest tests
- Registry entry

  • All module files created (see template 01)
  • Every image uses the ResponsiveImage component — no raw &lt;img&gt; or &lt;picture&gt; tags
  • Every image has both imageDesktop and imageMobile variants in props and types
  • LCP candidate image has priority={true} and loading="eager"
  • All below-fold images have loading="lazy"
  • srcset generated with appropriate width breakpoints for each image context
  • sizes attribute accurately reflects rendered size at each breakpoint
  • Focal point data from CMS applied to object-position style
  • Aspect ratios maintained to prevent CLS (explicit width and height or aspect-ratio CSS)
  • Cloudflare image loader configured for URL transforms (format, quality, width)
  • Desktop images stay under 500KB budget at target dimensions
  • Mobile images stay under 200KB budget at target dimensions
  • Images render correctly when CMS provides only one variant (graceful fallback)
  • Mapper extracts both imageDesktop and imageMobile from Umbraco response, with null handling
  • Storybook stories include: with images, with missing mobile image, with focal point offset
  • Unit tests verify: image props passed correctly, priority flag on LCP image, lazy on others
  • All standard module acceptance criteria from template 01 also met
  • No layout shift when images load (CLS = 0 for image containers)

  1. Using raw &lt;img&gt; tags — Always use the ResponsiveImage component. It handles srcset, sizes, loader, and format negotiation.
  2. Missing imageMobile — Every image context requires both desktop and mobile variants. The CMS always provides both; the mapper must extract both.
  3. All images eager-loaded — Only the LCP image should be eager. Everything else lazy. Loading 10 eager images kills page performance.
  4. Missing sizes attribute — Without sizes, the browser downloads the largest image in the srcset. Always specify sizes matching your CSS layout.
  5. No width/height or aspect-ratio — The browser needs dimensions to reserve space. Without them, images cause layout shift (CLS).
  6. Ignoring focal point — The CMS editor sets a focal point for each image. Use it as object-position: {focalX}% {focalY}% for object-fit: cover images.
  7. Hardcoded image URLs in stories — Use placeholder images with correct aspect ratios, not production CDN URLs that may break.
  8. Forgetting mobile aspect ratio differs — Desktop 16:9 might become mobile 4:3 or 1:1. The design specifies different crops, not just scaled-down desktop.
  9. srcset widths too sparse — Include enough widths to cover common device widths: 320, 375, 414, 640, 750, 828, 1080, 1200, 1440, 1920.
  10. Not testing with slow connection — Image-heavy modules should be tested with Chrome DevTools throttled to “Slow 3G” to verify lazy loading and perceived performance.

Filled-in prompt for M11 Image Gallery:

Read the following context files first:
- docs/dev-frontend-guides/06_RESPONSIVE_IMAGES_PATTERN.md
- docs/dev-frontend-guides/03_MODULE_DEVELOPMENT_LIFECYCLE.md
- docs/dev-frontend-guides/05_BEM_SASS_THEMING.md
- docs/PRD/13_Media_and_Image_Pipeline.md
- docs/PRD/07_Modules_and_Templates.md
- packages/ui/src/ (find and read the ResponsiveImage component)
- packages/modules/src/m04-page-hero/ (all files in this directory)
- packages/modules/src/registry.ts
Now create a new image-heavy module with the following details:
**Module ID:** M11
**Module Name:** Image Gallery
**Figma Reference:** https://www.figma.com/design/abc123/Savoy-Design-System?node-id=3456-7890
**Umbraco Content Type Alias:** imageGallery
**Component Type:** Client (needs lightbox interaction)
**Description:**
A grid of thumbnail images that opens a full-screen lightbox on click. Used on hotel
detail pages, room pages, and experience pages. Typically 6-20 images. The grid shows
thumbnails; the lightbox shows full-resolution images with prev/next navigation.
**Image Contexts:**
| Context | Desktop Dimensions | Mobile Dimensions | Aspect Ratio | Loading | Position |
|---------|-------------------|-------------------|--------------|---------|----------|
| Grid thumbnail | 400x300 | 350x263 | 4:3 | lazy (first 6 eager) | varies |
| Lightbox full | 1440x960 | 750x500 | 3:2 | lazy (on demand) | n/a |
**Focal Point Handling:**
Thumbnails: use focal point for object-position with object-fit: cover.
Lightbox: show full image with object-fit: contain, no crop, focal point not needed.
**Props:**
- heading?: string — Optional section heading
- images: GalleryImage[] — Array of image objects:
- imageDesktop: ImageData — Desktop image with url, width, height, focalPoint
- imageMobile: ImageData — Mobile image with url, width, height, focalPoint
- alt: string — Alt text for accessibility
- caption?: string — Optional caption shown in lightbox
- columns?: 2 | 3 | 4 — Grid columns on desktop (default: 3)
**UI Components to Use:**
ResponsiveImage, Heading, Icon (close, arrow-left, arrow-right)
**Layout:**
Grid: CSS Grid, 3 columns desktop, 2 columns tablet, 2 columns mobile (smaller gap).
Thumbnails: 4:3 aspect ratio, object-fit cover, 8px gap desktop, 4px gap mobile.
Lightbox: fixed overlay, full viewport, dark background (rgba(0,0,0,0.9)),
centered image with max 90vw/90vh, prev/next arrows on sides, close button top-right,
caption below image, dot indicators or counter ("3 / 12").
**LCP Considerations:**
This module is typically below the fold. All thumbnails can be lazy loaded.
However, if the gallery appears above the fold, the first 6 thumbnails should be eager.
Lightbox images are always loaded on demand (when lightbox opens).
Create all required files:
1. packages/modules/src/m11-image-gallery/index.tsx
2. packages/modules/src/m11-image-gallery/m11-image-gallery.client.tsx
3. packages/modules/src/m11-image-gallery/ImageGallery.scss
4. packages/modules/src/m11-image-gallery/m11-image-gallery.types.ts
5. packages/modules/src/m11-image-gallery/m11-image-gallery.mapper.ts
6. packages/modules/src/m11-image-gallery/m11-image-gallery.stories.tsx
7. packages/modules/src/m11-image-gallery/m11-image-gallery.test.tsx
Register in packages/modules/src/registry.ts.
Key image requirements:
- Every image uses imageDesktop + imageMobile via ResponsiveImage
- Thumbnails: lazy by default, eager if above fold (configurable prop)
- Lightbox images: loaded only when lightbox is opened, preload adjacent slides
- Focal point applied to thumbnails via object-position
- Lightbox: object-fit contain, no focal point
- srcset widths: thumbnails [320, 400, 640, 800], lightbox [750, 1080, 1440, 1920]
- sizes: thumbnails "33vw" desktop / "50vw" mobile, lightbox "90vw"
- Lightbox: focus trap, Escape to close, ArrowLeft/Right to navigate, aria-modal="true"
- prefers-reduced-motion: no slide animation in lightbox, instant transitions