05 — BEM + SASS + Theming Conventions
Dev Guide — Savoy Signature Hotels
PRD refs:05_Design_System_and_Theming.md,07_Modules_and_Templates.md,04_Frontend_Architecture.md
1. Purpose
Section titled “1. Purpose”This guide defines the CSS architecture for the Savoy platform: BEM methodology for class naming, SASS for nesting and organization, and CSS custom properties for multi-site theming. Following these conventions ensures that all 8 hotel themes work with zero JavaScript overhead.
2. CSS Architecture Stack
Section titled “2. CSS Architecture Stack”| Layer | Tool | Role |
|---|---|---|
| Methodology | BEM (Block Element Modifier) | Predictable, flat class naming |
| Preprocessor | SASS (.scss) | Nesting, & shorthand for BEM, mixins |
| Theming | CSS Custom Properties (variables) | Per-site visual tokens, switchable via data-theme |
| Scoping | File per component | Each module has its own .scss file |
What we do NOT use:
- CSS Modules (
.module.css) — PRD 04 mentions these but PRD 07 specifies BEM + SASS - CSS-in-JS / styled-components — no runtime CSS
- Tailwind / utility classes — we use semantic BEM classes
- SASS variables for theme values — always CSS custom properties
3. BEM Naming Convention
Section titled “3. BEM Naming Convention”3.1 Structure
Section titled “3.1 Structure”.block.block__element.block--modifier.block__element--modifier3.2 Rules
Section titled “3.2 Rules”| BEM Part | Convention | Example |
|---|---|---|
| Block | Component name in kebab-case | .hero-slider |
| Element | __ separator | .hero-slider__title |
| Modifier | -- separator | .hero-slider--fullscreen |
| State | Modifier or is- prefix | .hero-slider--active, .is-visible |
3.3 Mapping Component to BEM
Section titled “3.3 Mapping Component to BEM”| Component (PascalCase) | BEM Block (kebab-case) |
|---|---|
HeroSlider | .hero-slider |
CardGrid | .card-grid |
BookingBar | .booking-bar |
RichTextBlock | .rich-text-block |
ResponsiveImage | .responsive-image |
4. SASS Structure
Section titled “4. SASS Structure”4.1 File Convention
Section titled “4.1 File Convention”Each module has a single .scss file named {PascalName}.scss:
packages/modules/src/m08-card-grid/CardGrid.scsspackages/ui/src/Button/Button.scss4.2 SASS Nesting with &
Section titled “4.2 SASS Nesting with &”Use SASS & to co-locate BEM elements and modifiers:
.card-grid { padding: var(--space-12) 0;
// Elements &__container { max-width: var(--container-max); margin: 0 auto; padding: 0 var(--container-padding); }
&__title { font-family: var(--font-heading); font-size: var(--text-3xl); color: var(--color-text); margin-bottom: var(--space-8);
// Responsive @media (min-width: 1024px) { font-size: var(--text-4xl); } }
&__grid { display: grid; gap: var(--space-6); }
&__card { background-color: var(--color-surface); border-radius: var(--radius-md); box-shadow: var(--shadow-sm); transition: box-shadow var(--transition-normal);
// Interaction &:hover { box-shadow: var(--shadow-md); } }
// Modifiers &--dark { background-color: var(--color-bg-dark); }
// Grid column modifiers &__grid--cols-2 { @media (min-width: 768px) { grid-template-columns: repeat(2, 1fr); } }
&__grid--cols-3 { @media (min-width: 768px) { grid-template-columns: repeat(2, 1fr); } @media (min-width: 1024px) { grid-template-columns: repeat(3, 1fr); } }
&__grid--cols-4 { @media (min-width: 640px) { grid-template-columns: repeat(2, 1fr); } @media (min-width: 1024px) { grid-template-columns: repeat(4, 1fr); } }}4.3 SASS Rules
Section titled “4.3 SASS Rules”| Rule | Rationale |
|---|---|
| Max 3 levels of nesting | Keeps selectors flat and predictable |
& for BEM only | Don’t nest .other-class inside — keep blocks independent |
| Media queries inside the element they modify | Co-locates responsive behavior |
| No SASS variables for theme values | Use CSS custom properties (var(--token)) exclusively |
No @extend | Produces unpredictable CSS; use mixins or class composition |
5. Theming with CSS Custom Properties
Section titled “5. Theming with CSS Custom Properties”5.1 How Theming Works
Section titled “5.1 How Theming Works”<html data-theme="savoy-palace"> <!-- Set by layout.tsx based on site --> <body> <section class="card-grid"> <!-- Uses var(--color-primary) --> ... </section> </body></html>When data-theme changes, all CSS variables resolve to the new theme’s values automatically. Zero JavaScript involved.
5.2 Token Usage in SCSS
Section titled “5.2 Token Usage in SCSS”Always use CSS variables, never raw values:
// CORRECT.hero-slider__title { font-family: var(--font-heading); font-size: var(--text-4xl); color: var(--color-text-inverse);}
// WRONG - hardcoded values.hero-slider__title { font-family: 'Playfair Display', serif; font-size: 2.25rem; color: #ffffff;}
// WRONG - SASS variables for theme values$font-heading: 'Playfair Display', serif;.hero-slider__title { font-family: $font-heading;}5.3 Token Categories Quick Reference
Section titled “5.3 Token Categories Quick Reference”| Category | Prefix | Example |
|---|---|---|
| Colors | --color- | var(--color-primary), var(--color-bg), var(--color-text) |
| Fonts | --font- | var(--font-heading), var(--font-body) |
| Font weights | --font-weight- | var(--font-weight-bold) |
| Text sizes | --text- | var(--text-2xl), var(--text-base) |
| Spacing | --space- | var(--space-4), var(--space-8) |
| Radius | --radius- | var(--radius-md), var(--radius-full) |
| Shadows | --shadow- | var(--shadow-sm), var(--shadow-lg) |
| Z-index | --z- | var(--z-header), var(--z-modal) |
| Transitions | --transition- | var(--transition-fast), var(--transition-normal) |
| Container | --container- | var(--container-max), var(--container-padding) |
| Line height | --leading- | var(--leading-tight), var(--leading-normal) |
6. Responsive Design
Section titled “6. Responsive Design”6.1 Mobile-First Approach
Section titled “6.1 Mobile-First Approach”Always write base styles for mobile, then enhance with @media (min-width: ...):
.hero-slider { // Mobile (default) padding: var(--space-8) var(--container-padding); min-height: 60vh;
// Tablet @media (min-width: 768px) { padding: var(--space-12) var(--container-padding); }
// Desktop @media (min-width: 1024px) { min-height: 80vh; }
// Large desktop @media (min-width: 1280px) { padding: var(--space-16) var(--container-padding); max-width: var(--container-max); margin: 0 auto; }}6.2 Breakpoints
Section titled “6.2 Breakpoints”| Name | Value | Use |
|---|---|---|
sm | 640px | Large phones (landscape) |
md | 768px | Tablets (portrait) |
lg | 1024px | Tablets (landscape), small laptops |
xl | 1280px | Desktops |
2xl | 1440px | Large desktops (max container) |
6.3 Container Pattern
Section titled “6.3 Container Pattern”Common pattern for module content containment:
.module-name { &__container { width: 100%; max-width: var(--container-max); margin-left: auto; margin-right: auto; padding-left: var(--container-padding); padding-right: var(--container-padding); }}7. Focus and Accessibility Styles
Section titled “7. Focus and Accessibility Styles”Global focus styles are defined in _base.scss (or _base.css):
*:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px;}Skip link:
.skip-link { position: absolute; top: -100%; left: var(--space-4); z-index: var(--z-toast); padding: var(--space-2) var(--space-4); background-color: var(--color-primary); color: var(--color-text-on-primary);
&:focus { top: var(--space-4); }}8. Anti-Patterns to Avoid
Section titled “8. Anti-Patterns to Avoid”| Anti-Pattern | Why | Correct Approach |
|---|---|---|
color: #1a365d | Breaks theming | color: var(--color-primary) |
$sass-var: 16px for theme values | Not theme-switchable | var(--space-4) |
.card-grid .card-grid__title | Over-specific selector | .card-grid__title (BEM is already specific) |
| Nesting 4+ levels deep | Hard to maintain | Max 3 levels with BEM & |
@extend .some-class | Unpredictable output | Use mixins or direct classes |
!important | Specificity war | Fix the selector instead |
style={{ color: 'red' }} in JSX | Inline styles bypass theming | Use BEM class + CSS variable |
@media (max-width: 768px) | Not mobile-first | @media (min-width: 768px) |