05 — Design System and Theming
PRD Document · Savoy Signature Hotels — Multi-Site Headless Platform
Version: 1.0 · Date: 2026-03-04
Related docs:04_Frontend_Architecture.md,03_MultiSite_and_Domains.md,A03_Design_Tokens.md,A04_Module_Catalog.md
1. Purpose
Section titled “1. Purpose”This document defines the design system architecture for the Savoy Signature platform. The system supports 8 distinct visual identities (one per hotel/site) while sharing a common set of components and modules. Theming is achieved through CSS custom properties (CSS variables) with per-site value overrides.
2. Design System Overview
Section titled “2. Design System Overview”2.1 Architecture
Section titled “2.1 Architecture”2.2 Key Principles
Section titled “2.2 Key Principles”| Principle | Description |
|---|---|
| Token-first | All visual values (colors, fonts, spacing) are defined as CSS variables — never hardcoded |
| Theme = CSS variables | Switching themes means switching a set of CSS variable values; no JS needed |
| Component-agnostic theming | Components reference tokens (var(--color-primary)), not specific values |
| Mobile-first | All components designed mobile-first, enhanced for larger viewports |
| Figma as source of truth | Design tokens extracted from Figma; Storybook mirrors Figma components |
| Zero runtime overhead | CSS Modules + CSS variables = no JavaScript for styling at runtime |
3. Design Tokens
Section titled “3. Design Tokens”3.1 Token Categories
Section titled “3.1 Token Categories”| Category | Token Prefix | Examples | Scope |
|---|---|---|---|
| Color | --color- | --color-primary, --color-bg, --color-text | Per-theme |
| Typography | --font-, --text- | --font-heading, --text-base-size, --text-line-height | Per-theme (fonts), Shared (sizes) |
| Spacing | --space- | --space-xs, --space-sm, --space-md, --space-lg, --space-xl | Shared |
| Breakpoints | --breakpoint- | --breakpoint-sm: 640px, --breakpoint-lg: 1024px | Shared |
| Border Radius | --radius- | --radius-sm, --radius-md, --radius-full | Shared |
| Shadow | --shadow- | --shadow-sm, --shadow-md, --shadow-lg | Per-theme |
| Z-Index | --z- | --z-header, --z-modal, --z-overlay | Shared |
| Transition | --transition- | --transition-fast, --transition-normal | Shared |
3.2 Base Tokens (Shared)
Section titled “3.2 Base Tokens (Shared)”:root { /* ── Spacing Scale ── */ --space-0: 0; --space-1: 0.25rem; /* 4px */ --space-2: 0.5rem; /* 8px */ --space-3: 0.75rem; /* 12px */ --space-4: 1rem; /* 16px */ --space-5: 1.25rem; /* 20px */ --space-6: 1.5rem; /* 24px */ --space-8: 2rem; /* 32px */ --space-10: 2.5rem; /* 40px */ --space-12: 3rem; /* 48px */ --space-16: 4rem; /* 64px */ --space-20: 5rem; /* 80px */ --space-24: 6rem; /* 96px */ --space-32: 8rem; /* 128px */
/* ── Breakpoints (reference only, used in media queries) ── */ /* sm: 640px | md: 768px | lg: 1024px | xl: 1280px | 2xl: 1440px */
/* ── Typography Scale ── */ --text-xs: 0.75rem; /* 12px */ --text-sm: 0.875rem; /* 14px */ --text-base: 1rem; /* 16px */ --text-lg: 1.125rem; /* 18px */ --text-xl: 1.25rem; /* 20px */ --text-2xl: 1.5rem; /* 24px */ --text-3xl: 1.875rem; /* 30px */ --text-4xl: 2.25rem; /* 36px */ --text-5xl: 3rem; /* 48px */ --text-6xl: 3.75rem; /* 60px */
/* ── Line Heights ── */ --leading-tight: 1.15; --leading-snug: 1.3; --leading-normal: 1.5; --leading-relaxed: 1.75;
/* ── Border Radius ── */ --radius-none: 0; --radius-sm: 0.25rem; --radius-md: 0.5rem; --radius-lg: 1rem; --radius-full: 9999px;
/* ── Z-Index ── */ --z-base: 0; --z-header: 100; --z-dropdown: 200; --z-overlay: 300; --z-modal: 400; --z-toast: 500;
/* ── Transitions ── */ --transition-fast: 150ms ease; --transition-normal: 250ms ease; --transition-slow: 400ms ease;
/* ── Container ── */ --container-max: 1440px; --container-padding: var(--space-4);}
@media (min-width: 768px) { :root { --container-padding: var(--space-6); }}
@media (min-width: 1280px) { :root { --container-padding: var(--space-8); }}3.3 Theme-Specific Tokens (Example: Savoy Palace)
Section titled “3.3 Theme-Specific Tokens (Example: Savoy Palace)”[data-theme="savoy-palace"] { /* ── Colors ── */ --color-primary: #1a365d; --color-primary-light: #2a5a8e; --color-primary-dark: #0f2340; --color-secondary: #c9a96e; --color-secondary-light: #d4bc8e; --color-secondary-dark: #b08f4f; --color-accent: #e8d5b7;
--color-bg: #faf9f7; --color-bg-alt: #f2efe9; --color-bg-dark: #1a1a1a; --color-surface: #ffffff; --color-surface-elevated: #ffffff;
--color-text: #1a1a1a; --color-text-muted: #6b7280; --color-text-inverse: #ffffff; --color-text-on-primary: #ffffff; --color-text-on-secondary: #1a1a1a;
--color-border: #e5e2dc; --color-border-strong: #c9c4bb;
--color-success: #059669; --color-warning: #d97706; --color-error: #dc2626;
/* ── Typography ── */ --font-heading: 'Playfair Display', serif; --font-body: 'Inter', sans-serif; --font-accent: 'Cormorant Garamond', serif;
--font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700;
/* ── Shadows ── */ --shadow-sm: 0 1px 2px rgba(26, 54, 93, 0.05); --shadow-md: 0 4px 12px rgba(26, 54, 93, 0.08); --shadow-lg: 0 8px 24px rgba(26, 54, 93, 0.12);}Complete token tables for all 8 themes in
A03_Design_Tokens.md
4. Theme Loading
Section titled “4. Theme Loading”4.1 Theme File Structure
Section titled “4.1 Theme File Structure”packages/themes/src/├── _base.css # Shared tokens (all themes inherit)├── savoy-signature.css # [data-theme="savoy-signature"]├── savoy-palace.css # [data-theme="savoy-palace"]├── royal-savoy.css # [data-theme="royal-savoy"]├── saccharum.css # [data-theme="saccharum"]├── the-reserve.css # [data-theme="the-reserve"]├── calheta-beach.css # [data-theme="calheta-beach"]├── gardens.css # [data-theme="gardens"]├── hotel-next.css # [data-theme="hotel-next"]└── index.css # Imports _base.css + all themes4.2 Loading Strategy
Section titled “4.2 Loading Strategy”All themes are bundled in a single CSS file. The active theme is selected via the data-theme attribute on <html>:
import '@savoy/themes/src/index.css';import { headers } from 'next/headers';import { getSiteConfig } from '@/helpers/site-resolver';import { loadFont } from '@/lib/fonts';
export default async function RootLayout({ children }: { children: React.ReactNode }) { const headersList = await headers(); const siteKey = headersList.get('x-site-key') || 'savoy-signature'; const site = getSiteConfig(siteKey); const fontClasses = loadFont(site.theme);
return ( <html lang={site.defaultLocale} data-theme={site.theme}> <body className={`site-${site.key} ${fontClasses}`}> {children} </body> </html> );}4.3 Font Loading
Section titled “4.3 Font Loading”import { Playfair_Display, Inter, Cormorant_Garamond } from 'next/font/google';
const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-heading', display: 'swap' });const inter = Inter({ subsets: ['latin'], variable: '--font-body', display: 'swap' });const cormorant = Cormorant_Garamond({ subsets: ['latin'], weight: ['400', '600'], variable: '--font-accent', display: 'swap' });
// Map theme → font classesconst THEME_FONTS: Record<string, string> = { 'savoy-palace': `${playfair.variable} ${inter.variable} ${cormorant.variable}`, 'savoy-signature': `${playfair.variable} ${inter.variable}`, // ... other themes};
export function loadFont(theme: string): string { return THEME_FONTS[theme] || THEME_FONTS['savoy-signature'];}5. Responsive Strategy
Section titled “5. Responsive Strategy”5.1 Breakpoints
Section titled “5.1 Breakpoints”| Name | Width | Target |
|---|---|---|
sm | 640px | Large phones (landscape) |
md | 768px | Tablets (portrait) |
lg | 1024px | Tablets (landscape), small laptops |
xl | 1280px | Desktops |
2xl | 1440px | Large desktops (max container width) |
5.2 Mobile-First Approach
Section titled “5.2 Mobile-First Approach”/* Example: mobile-first responsive module */.heroSlider { padding: var(--space-8) var(--container-padding); display: flex; flex-direction: column; gap: var(--space-4);}
@media (min-width: 768px) { .heroSlider { padding: var(--space-12) var(--container-padding); flex-direction: row; gap: var(--space-8); }}
@media (min-width: 1280px) { .heroSlider { padding: var(--space-16) var(--container-padding); max-width: var(--container-max); margin: 0 auto; }}5.3 Container Strategy
Section titled “5.3 Container Strategy”/* Shared container utility */.container { width: 100%; max-width: var(--container-max); margin-left: auto; margin-right: auto; padding-left: var(--container-padding); padding-right: var(--container-padding);}6. Storybook
Section titled “6. Storybook”6.1 Configuration
Section titled “6.1 Configuration”Storybook runs as a separate app in the monorepo, importing from packages/ui and packages/modules:
apps/storybook/├── .storybook/│ ├── main.ts # Storybook config (refs to packages)│ ├── preview.ts # Global decorators (theme provider)│ └── theme-decorator.tsx # Theme switcher for Storybook toolbar├── package.json└── tsconfig.json6.2 Theme Decorator
Section titled “6.2 Theme Decorator”A Storybook toolbar addon lets reviewers switch between all 8 site themes:
import React from 'react';import '@savoy/themes/src/index.css';
const THEMES = [ 'savoy-signature', 'savoy-palace', 'royal-savoy', 'saccharum', 'the-reserve', 'calheta-beach', 'gardens', 'hotel-next',];
export const ThemeDecorator = (Story: any, context: any) => { const theme = context.globals.theme || 'savoy-palace';
return ( <div data-theme={theme}> <Story /> </div> );};6.3 Story Convention
Section titled “6.3 Story Convention”import type { Meta, StoryObj } from '@storybook/react';import { HeroSlider } from './HeroSlider';
const meta: Meta<typeof HeroSlider> = { title: 'Modules/M05 — Hero Slider', component: HeroSlider, tags: ['autodocs'], parameters: { layout: 'fullscreen', design: { type: 'figma', url: 'https://www.figma.com/file/...', // Link to Figma design }, },};
export default meta;type Story = StoryObj<typeof HeroSlider>;
export const Default: Story = { args: { title: 'Welcome to Savoy Palace', subtitle: 'Luxury redefined in Madeira', slides: [ { image: { src: '/placeholder-1.jpg', alt: 'Pool view', width: 1920, height: 1080 }, caption: 'Infinity pool with ocean views', }, ], autoplay: true, interval: 5000, },};
export const WithCTA: Story = { args: { ...Default.args, slides: [ { ...Default.args!.slides![0], cta: { label: 'Book Now', href: '/pt/rooms' }, }, ], },};
export const MultipleSlides: Story = { args: { ...Default.args, slides: [ Default.args!.slides![0], { image: { src: '/placeholder-2.jpg', alt: 'Lobby', width: 1920, height: 1080 }, caption: 'Grand lobby' }, { image: { src: '/placeholder-3.jpg', alt: 'Room', width: 1920, height: 1080 }, caption: 'Deluxe suite' }, ], },};6.4 Deployment
Section titled “6.4 Deployment”Storybook is deployed only to internal WYcreative environments (DEV and STAGE). It is not deployed to QA or PROD.
| Environment | URL | Trigger |
|---|---|---|
| DEV | savoy.dev-storybook.wycreative.com | Merge to develop |
| STAGE | savoy.stage-storybook.wycreative.com | Merge to staging |
7. Acceptance Criteria
Section titled “7. Acceptance Criteria”- All 8 themes render correctly in Storybook via theme switcher
- Switching
data-themechanges all visual properties (colors, fonts, shadows) without JS - Base tokens (spacing, breakpoints) are consistent across all themes
- Font loading causes zero layout shift (FOUT/FOIT)
- All components work at all breakpoints (sm → 2xl)
- Storybook deploys automatically and is accessible to the team
- Theme CSS files add less than 5KB each (gzipped)
- Design tokens match Figma specifications
Next document: 06_Content_Modeling_Umbraco.md