Skip to content

M11 — Banner: Development Prompt

Read the following context files first:

  • docs/PRD/07_Modules_and_Templates.md
  • docs/dev-frontend-guides/03_MODULE_DEVELOPMENT_LIFECYCLE.md
  • docs/dev-frontend-guides/05_BEM_SASS_THEMING.md
  • docs/dev-frontend-guides/01_FIGMA_TO_CODE_WORKFLOW.md
  • docs/dev-frontend-guides/04_STORYBOOK_FIRST_DEVELOPMENT.md
  • docs/dev-frontend-guides/06_RESPONSIVE_IMAGES_PATTERN.md
  • packages/modules/src/m04-page-hero/ (reference implementation — similar full-bleed image module)
  • packages/modules/src/registry.ts
  • packages/themes/src/_base.css
  • packages/themes/src/savoy-palace.css (reference for module token pattern)
  • packages/ui/src/ (scan for available UI components, especially ResponsiveImage and CtaLink)

FieldValue
Module IDM11
Module NameBanner
Folderm11-banner
PascalCaseBanner
camelCase (Registry Key / Umbraco Alias)banner
BEM Block.banner
Component TypeStatic (Server Component — no interactivity, no client-side state)

Description: Full-width banner module with a background image and dark overlay (Light variant) or a solid tinted background (Medium variant). Content sits in a left or right aligned text column containing an optional etiqueta (pill-shaped badge image), headline, body text, and up to two CTAs. An optional decorative symbol image appears on the side opposite the text column, desktop only. Supports two height sizes (Small / Big) that also change the headline tag (H6 / H5) and layout column width. All fields are optional.

Figma Links:

VariantURL
Desktop (default — Small, Light, Left)https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1110-1817&m=dev
Mobilehttps://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1112-1930&m=dev
All desktop variants (8 combinations)https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1110-1818&m=dev
Additional variantshttps://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1721-97850&m=dev

All props are optional. siteKey and locale are always injected by ModuleRenderer.

import type { HtmlHeading, CtaLinkWithElement } from '@savoy/cms-client';
import type { ResponsiveImageProps } from '@savoy/ui';
import type { SiteKey } from '@savoy/cms-client';
export interface BannerProps {
// --- Layout / variant selectors ---
background?: 'light' | 'medium'; // 'light' = image+overlay | 'medium' = solid tinted bg. Default: 'light'
size?: 'small' | 'big'; // 'small' = 608px / H6 | 'big' = 790px / H5. Default: 'small'
textPosition?: 'left' | 'right'; // Position of the text column. Default: 'left'
// --- Content fields (all optional) ---
etiqueta?: ResponsiveImageProps; // Pill-shaped badge/logo image (134x50px display size)
headline?: HtmlHeading; // Heading text — H6 for Small, H5 for Big (editor controls tag)
body?: string; // Body paragraph text (Body M)
cta?: CtaLinkWithElement; // Primary CTA (filled button)
ctaSecondary?: CtaLinkWithElement; // Secondary CTA (link/underline style)
symbolImage?: ResponsiveImageProps; // Decorative image opposite text column (desktop only)
// --- Background image (used when background = 'light') ---
imageDesktop?: ResponsiveImageProps['imageDesktop'];
imageMobile?: ResponsiveImageProps['imageMobile'];
// --- Standard module props ---
siteKey: SiteKey;
locale: string;
moduleId?: string; // injected by page renderer
}

Prop notes:

  • background: 'medium' replaces the image+overlay with var(--color-bg-alt) (tinted surface). When medium, imageDesktop/imageMobile are ignored.
  • size: 'big' increases module height, widens the layout grid (12 columns vs 10), and changes the headline font from --text-5xl (H6) to --text-6xl (H5). The editor controls the semantic HTML tag via headline.html.
  • etiqueta is a Media Picker image displayed as a pill (border-radius: 100px). The image itself provides the visual (logo, award badge, etc.).
  • symbolImage is decorative — hidden on mobile, shown on the side opposite textPosition on desktop.
  • When cta is absent, ctaSecondary should also not render (secondary CTA cannot stand alone without primary).

SizeBackgroundText PositionHeightHeadlineColumns
SmallLightLeft608pxH6 (40px)10 col (996px)
SmallLightRight608pxH6 (40px)10 col (996px)
SmallMediumLeft608pxH6 (40px)10 col (996px)
SmallMediumRight608pxH6 (40px)10 col (996px)
BigLightLeft790pxH5 (48px)12 col (1360px)
BigLightRight790pxH5 (48px)12 col (1360px)
BigMediumLeft790pxH5 (48px)12 col (1360px)
BigMediumRight790pxH5 (48px)12 col (1360px)
  • Height: 600px
  • No symbolImage — hidden on mobile (SCSS display: none, not conditional render)
  • CTAs stack vertically (primary full-width, secondary centered below)
  • Padding: 24px horizontal
  • Content max-width: 312px (5 columns)

Desktop structure (BEM):

<section class="banner banner--light banner--small banner--text-left">
<div class="banner__background"> ← full-bleed image (light) or solid bg (medium)
<div class="banner__overlay"> ← dark 40% overlay (light variant only)
<div class="banner__container"> ← centered, 10 or 12 columns wide
<div class="banner__inner"> ← flex row, gap 102px (small) / 126px (big)
<div class="banner__content"> ← text column, ~486px (small) / ~588px (big)
<img class="banner__etiqueta"> ← optional pill image
<h6 class="banner__headline"> ← or h5 when size=big
<p class="banner__body">
<div class="banner__ctas">
<a class="banner__cta"> ← filled
<a class="banner__cta-secondary"> ← link
<div class="banner__symbol"> ← optional decorative image (opposite side)

SCSS modifier classes on root &lt;section&gt;:

  • banner--light / banner--medium — background variant
  • banner--small / banner--big — size variant
  • banner--text-left / banner--text-right — text position

Mobile:

  • banner__symbol hidden via display: none at all breakpoints below lg
  • banner__ctas switches to flex-direction: column; align-items: center
  • Primary CTA becomes width: 100%
  • Content column takes full width (no side-by-side layout)

// Background
--color-bg // Light variant: page background fallback
--color-bg-alt // Medium variant: tinted surface (e.g. beige)
// Text (white on Light; dark on Medium)
--color-text
--color-text-on-dark // use for light variant text
// Typography
--font-heading
--font-body
--text-5xl // H6 font-size (small, ~40px)
--text-6xl // H5 font-size (big, ~48px)
--text-base // Body M
--leading-tight
--leading-normal
// Spacing
--space-6 // 24px — mobile padding, cta gap
--space-10 // 40px — gap between content groups
--container-max
--container-padding
--container-padding-md
--container-padding-lg

ComponentSourceUsage
ResponsiveImage@savoy/uiBackground image (light), etiqueta, symbolImage
CtaLink@savoy/uiPrimary and secondary CTAs
HtmlHeading rendering patterninline (see frontend-module rules)Renders headline with dynamic tag from CMS. Content is trusted CMS output — see .claude/rules/frontend-module.md for the safe rendering pattern.

Do NOT create custom button styles — use CtaLink from @savoy/ui with appropriate as and class props.


Banner.mapper.ts
import { mapHtmlHeading } from '@savoy/cms-client';
import type { BannerProps } from './Banner.types';
export function mapBanner(
element: UmbracoElement
): Omit<BannerProps, 'siteKey' | 'locale'> {
const p = element.properties;
const ctaLink = p.cta?.[0];
const ctaSecondaryLink = p.ctaSecondary?.[0];
return {
active: p.active !== false,
background: p.background ?? 'light',
size: p.size ?? 'small',
textPosition: p.textPosition ?? 'left',
headline: p.headline ? mapHtmlHeading(p.headline) : undefined,
body: p.body ?? undefined,
etiqueta: p.etiquetaDesktop
? {
imageDesktop: { url: p.etiquetaDesktop.url, width: 134, height: 50, alt: p.etiquetaDesktop.name ?? '' },
imageMobile: { url: (p.etiquetaMobile ?? p.etiquetaDesktop).url, width: 134, height: 50, alt: '' },
}
: undefined,
imageDesktop: p.imageDesktop
? { url: p.imageDesktop.url, width: 1440, height: 790, alt: p.imageDesktop.name ?? '' }
: undefined,
imageMobile: p.imageMobile
? { url: p.imageMobile.url, width: 375, height: 600, alt: '' }
: undefined,
symbolImage: p.symbolImageDesktop
? {
imageDesktop: { url: p.symbolImageDesktop.url, width: 501, height: 626, alt: p.symbolImageDesktop.name ?? '' },
imageMobile: { url: (p.symbolImageMobile ?? p.symbolImageDesktop).url, width: 375, height: 400, alt: '' },
}
: undefined,
cta: ctaLink
? { label: ctaLink.name, href: ctaLink.url, as: 'a' }
: undefined,
ctaSecondary: ctaSecondaryLink
? { label: ctaSecondaryLink.name, href: ctaSecondaryLink.url, as: 'a' }
: undefined,
};
}

Required variants:

StoryPurpose
Defaultbackground: ‘light’, size: ‘small’, textPosition: ‘left’, all fields, realistic hotel content
TextRighttextPosition: ‘right’, all fields
SizeBigsize: ‘big’, textPosition: ‘left’, all fields
BackgroundMediumbackground: ‘medium’, all fields
MinimalContentheadline + cta only — no etiqueta, no body, no symbolImage
NoCtaheadline + body, no CTAs, no symbolImage
FigmaFidelityExact Figma content verbatim (Lorem ipsum as-is) — visual test target

FigmaFidelity story parameters:

parameters: {
pixelPerfect: {
threshold: { desktop: 0.05, mobile: 0.10 },
viewports: ['desktop', 'mobile'],
themes: ['savoy-palace'],
},
},

Mock data guidelines:

  • Use hotel / restaurant / room descriptions as realistic content
  • siteKey: 'savoy-palace', locale: 'pt'
  • Image URLs: use Unsplash or placeholder services with correct aspect ratios

Alias: banner Icon: icon-banner Tab: Content (sortOrder 1)

#Property NameAliasEditorNotes
0ActiveactiveToggleDefault ON. First property.
1BackgroundbackgroundDropdown (light, medium)Default: light
2SizesizeDropdown (small, big)Default: small
3Text PositiontextPositionDropdown (left, right)Default: left
4HeadlineheadlineSingle Html Heading Block ListOptional
5BodybodyTextareaOptional
6CTActaURL PickerOptional
7CTA SecondaryctaSecondaryURL PickerOptional
8Etiqueta DesktopetiquetaDesktopMedia PickerOptional — pill badge image
9Etiqueta MobileetiquetaMobileMedia PickerOptional — falls back to desktop
10Image DesktopimageDesktopMedia PickerUsed when background = ‘light’
11Image MobileimageMobileMedia PickerUsed when background = ‘light’
12Symbol Image DesktopsymbolImageDesktopMedia PickerOptional
13Symbol Image MobilesymbolImageMobileMedia PickerOptional — falls back to desktop

Block List label expression: Banner - {{headline}} (If editor leaves headline blank, add a blockLabel Textstring property at position 1 and use Banner - {{blockLabel}}.)

SVG Thumbnail: apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m11-banner.svg


packages/modules/src/m11-banner/
index.ts
Banner.tsx
Banner.scss
Banner.types.ts
Banner.mapper.ts
Banner.stories.tsx
Banner.test.tsx

Register in packages/modules/src/registry.ts:

banner: { component: Banner, mapper: mapBanner, moduleId: 'M11' },

  1. symbolImage on mobile — use display: none in SCSS (not conditional render) so the DOM element exists for potential JS hooks but is hidden
  2. Background Medium ignores images — when background === 'medium', do not render &lt;img&gt; background or overlay element
  3. Size modifies layout AND typographybanner--big must change both column width AND font-size token (--text-5xl--text-6xl)
  4. Text colour on Medium — headline and body are dark-coloured on Medium variant (no image overlay), use --color-text not --color-text-on-dark
  5. Etiqueta border-radiusborder-radius: var(--radius-full) (100px) — use the token, not a hardcoded value
  6. Do not hardcode heights — use min-height + padding so content is never clipped if text overflows
  7. CTA secondary guard — if cta prop is absent, ctaSecondary must also be hidden (check cta && before rendering ctaSecondary)