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.
Context Loading
Section titled “Context Loading”Read these files before executing the prompt:
docs/dev-frontend-guides/06_RESPONSIVE_IMAGES_PATTERN.mddocs/dev-frontend-guides/03_MODULE_DEVELOPMENT_LIFECYCLE.mddocs/dev-frontend-guides/05_BEM_SASS_THEMING.mddocs/PRD/13_Media_and_Image_Pipeline.mddocs/PRD/07_Modules_and_Templates.mdpackages/ui/src/ # find ResponsiveImage componentpackages/modules/src/m04-page-hero/ # existing image-heavy module examplepackages/modules/src/registry.tsPrompt Template
Section titled “Prompt Template”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 entryAcceptance Criteria
Section titled “Acceptance Criteria”- All module files created (see template 01)
- Every image uses the
ResponsiveImagecomponent — no raw<img>or<picture>tags - Every image has both
imageDesktopandimageMobilevariants in props and types - LCP candidate image has
priority={true}andloading="eager" - All below-fold images have
loading="lazy" -
srcsetgenerated with appropriate width breakpoints for each image context -
sizesattribute accurately reflects rendered size at each breakpoint - Focal point data from CMS applied to
object-positionstyle - Aspect ratios maintained to prevent CLS (explicit
widthandheightoraspect-ratioCSS) - 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
imageDesktopandimageMobilefrom 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)
Common Pitfalls
Section titled “Common Pitfalls”- Using raw
<img>tags — Always use theResponsiveImagecomponent. It handles srcset, sizes, loader, and format negotiation. - Missing
imageMobile— Every image context requires both desktop and mobile variants. The CMS always provides both; the mapper must extract both. - All images eager-loaded — Only the LCP image should be
eager. Everything elselazy. Loading 10 eager images kills page performance. - Missing
sizesattribute — Withoutsizes, the browser downloads the largest image in the srcset. Always specify sizes matching your CSS layout. - No
width/heightoraspect-ratio— The browser needs dimensions to reserve space. Without them, images cause layout shift (CLS). - Ignoring focal point — The CMS editor sets a focal point for each image. Use it as
object-position: {focalX}% {focalY}%forobject-fit: coverimages. - Hardcoded image URLs in stories — Use placeholder images with correct aspect ratios, not production CDN URLs that may break.
- 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.
- srcset widths too sparse — Include enough widths to cover common device widths: 320, 375, 414, 640, 750, 828, 1080, 1200, 1440, 1920.
- 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.
Example
Section titled “Example”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 hoteldetail pages, room pages, and experience pages. Typically 6-20 images. The grid showsthumbnails; 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.tsx2. packages/modules/src/m11-image-gallery/m11-image-gallery.client.tsx3. packages/modules/src/m11-image-gallery/ImageGallery.scss4. packages/modules/src/m11-image-gallery/m11-image-gallery.types.ts5. packages/modules/src/m11-image-gallery/m11-image-gallery.mapper.ts6. packages/modules/src/m11-image-gallery/m11-image-gallery.stories.tsx7. 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