Skip to content

01 — Figma-to-Code Workflow

Dev Guide — Savoy Signature Hotels
PRD refs: 04_Frontend_Architecture.md, 05_Design_System_and_Theming.md, 07_Modules_and_Templates.md


This guide defines the complete workflow for translating a Figma design into a production-ready React component. The workflow ensures that every component is token-based, theme-aware, responsive, accessible, and documented in Storybook.


Figma Design

[Step 1] Analyze Figma Structure

[Step 2] Extract Design Tokens (if new)

[Step 3] Identify Component Hierarchy

[Step 4] Create Storybook Story (skeleton)

[Step 5] Implement React Component + SCSS

[Step 6] Write Mapper + Types (CMS integration)

[Step 7] Register in Module Registry

[Step 8] Validate in Storybook (all themes + viewports)

[Step 9] Write Tests

[Step 10] PR Review


Before writing any code, analyze the Figma design:

Checklist:

  • What is the module ID? (e.g., M05 — Hero Slider)
  • Is it a UI component (packages/ui) or a CMS module (packages/modules)?
  • Which breakpoints are designed? (Mobile 375px, Tablet 768px, Desktop 1440px)
  • Are there Desktop AND Mobile image variants?
  • What interactive behaviors exist? (hover states, animations, transitions)
  • Does this need to be a Client Component? (interactivity = yes)
  • What accessibility requirements apply? (keyboard nav, ARIA roles, focus management)

Decision Tree — UI Component vs. Module:

YES

NO

Is it driven by CMS content

- Umbraco -?

packages/modules/ (Module)

Needs: mapper, types, registry entry

packages/ui/ (UI Component)

Reusable primitive (Button, Input, Card, Image)

If the Figma design introduces values NOT already in the token system:

  1. Check packages/themes/src/_base.css for shared tokens
  2. Check packages/themes/src/{site}.css for theme-specific tokens
  3. If new values exist, add them following the token naming convention (see Guide 02)

Rule: NEVER hardcode color, font, spacing, or shadow values. Always use var(--token-name).

Break the Figma design into its component tree:

M05 Hero Slider

HeroSlider (Module — Server or Client)

HeroSlider__content

HeroSlider__slides

Title (h1)

Subtitle (p)

Slide[]

ResponsiveImage (packages/ui)

Caption (p)

CTA (a) — optional

Rules:

  • Modules compose UI components, never the reverse
  • If a sub-element is reusable across modules, extract it to packages/ui
  • Interactive sub-components go in a separate .client.tsx file

Step 4: Create Storybook Story (skeleton first)

Section titled “Step 4: Create Storybook Story (skeleton first)”

Write the story BEFORE implementing the component. This is “Storybook-first development”:

packages/modules/src/m05-hero-slider/HeroSlider.stories.tsx
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/...?node-id=XX',
},
},
};
export default meta;
type Story = StoryObj<typeof HeroSlider>;
export const Default: Story = {
args: {
// Fill with realistic mock data matching the Figma
},
};

Follow the file structure from PRD 07:

packages/modules/src/m05-hero-slider/
index.ts
HeroSlider.tsx
HeroSlider.client.tsx (if interactive)
HeroSlider.scss
HeroSlider.types.ts
HeroSlider.mapper.ts
HeroSlider.stories.tsx
HeroSlider.test.tsx

Implementation order within a module:

  1. .types.ts — Define the props interface
  2. .tsx — Implement the component (use tokens, BEM classes)
  3. .scss — Style with BEM + SASS + CSS variables
  4. .stories.tsx — Complete all story variants
  5. .mapper.ts — Map Umbraco API response to props
  6. .test.tsx — Unit tests for mapper + component rendering
  7. index.ts — Named export

The mapper is the bridge between CMS data and component props:

HeroSlider.types.ts
export interface HeroSliderProps {
title: string;
subtitle?: string;
slides: HeroSlide[];
autoplay?: boolean;
interval?: number;
siteKey: string;
locale: string;
}
export interface HeroSlide {
imageDesktop: { url: string; width: number; height: number };
imageMobile: { url: string; width: number; height: number };
altText: string;
caption?: string;
cta?: { label: string; href: string };
}
HeroSlider.mapper.ts
import { UmbracoElement } from '@savoy/cms-client';
import { HeroSliderProps } from './HeroSlider.types';
export function mapHeroSlider(element: UmbracoElement): Omit<HeroSliderProps, 'siteKey' | 'locale'> {
return {
title: element.properties.title,
subtitle: element.properties.subtitle,
slides: element.properties.slides.map((slide: any) => ({
imageDesktop: {
url: slide.imageDesktop?.url,
width: slide.imageDesktop?.width,
height: slide.imageDesktop?.height,
},
imageMobile: {
url: slide.imageMobile?.url,
width: slide.imageMobile?.width,
height: slide.imageMobile?.height,
},
altText: slide.altText || '',
caption: slide.caption,
cta: slide.cta ? { label: slide.cta.name, href: slide.cta.url } : undefined,
})),
autoplay: element.properties.autoplay ?? true,
interval: element.properties.interval ?? 5000,
};
}
packages/modules/src/registry.ts
import { HeroSlider } from './m05-hero-slider';
import { mapHeroSlider } from './m05-hero-slider/HeroSlider.mapper';
export const moduleRegistry = {
// ... existing modules
heroSlider: { component: HeroSlider, mapper: mapHeroSlider, moduleId: "M05" },
};

Validation checklist:

  • Renders correctly in all 8 themes (use Storybook theme switcher)
  • Responsive at all breakpoints: 375px, 768px, 1024px, 1280px, 1440px
  • All variants/states visible (default, with CTA, multiple slides, etc.)
  • Interactive behaviors work (if Client Component)
  • No hardcoded colors, fonts, or spacing (only CSS variables)
  • Figma link present in Storybook story parameters

Step 8.5: Pixel Perfect Visual Testing (MANDATORY)

Section titled “Step 8.5: Pixel Perfect Visual Testing (MANDATORY)”

No module is complete without passing visual tests. This step validates your implementation against the Figma design using automated screenshot comparison.

  1. Fetch Figma baselines — Use Figma MCP get_screenshot to capture the design at desktop (1440x900) and mobile (375x812). Save to apps/storybook/__figma_baselines__/{storyId}/
  2. Register mapping — Add story → Figma node in .storybook/visual-testing/figma-mapping.ts
  3. Run visual testspnpm --filter storybook visual:test (Storybook must be running)
  4. Review diffs — Open the Pixel Perfect panel in Storybook to see diff % and diff overlay images
  5. Iterate — Fix CSS until diff is within threshold (default: 0.5%)
  6. Promote baselines — Click “Baseline” in the panel to capture the current state as regression baseline
  7. Commit baselines__figma_baselines__/ and __visual_snapshots__/ are version-controlled

Visual test checklist:

  • Figma baselines fetched for desktop and mobile viewports
  • Figma mapping registered in figma-mapping.ts
  • pnpm --filter storybook visual:test passes (diff within threshold)
  • Regression baselines promoted and committed
  • Pixel Perfect panel shows all tests passing
HeroSlider.test.tsx
import { describe, it, expect } from 'vitest';
import { mapHeroSlider } from './HeroSlider.mapper';
describe('mapHeroSlider', () => {
it('maps API response to component props', () => {
const apiResponse = { /* mock Umbraco element */ };
const props = mapHeroSlider(apiResponse);
expect(props.title).toBe('Expected Title');
expect(props.slides).toHaveLength(2);
});
it('defaults autoplay to true when not specified', () => {
const apiResponse = { properties: { slides: [] } };
const props = mapHeroSlider(apiResponse);
expect(props.autoplay).toBe(true);
});
});

PR checklist (max 400 lines changed):

  • Linked to Zoho Project task
  • All files follow naming convention
  • No hardcoded values
  • Stories render in all themes
  • Tests pass (pnpm test)
  • Pixel Perfect visual tests pass (pnpm --filter storybook visual:test)
  • Figma baselines and regression baselines committed (if new module)
  • ESLint + TypeScript clean
  • Conventional commit message (feat: Add M05 Hero Slider module)

PitfallSolution
Hardcoded #1a365d instead of var(--color-primary)Always use CSS variable tokens
Forgot mobile image variantEvery image module needs imageDesktop + imageMobile
Made everything a Client ComponentDefault to Server Component. Only add 'use client' when interactivity requires it
Skipped Storybook storiesStories are mandatory for every module — they drive visual regression tests
Wrote mapper without typesAlways define .types.ts first, then implement mapper against those types
Nested modules inside modulesModules compose UI components only. Never import one module inside another

5. Figma-to-CSS Token Mapping Quick Reference

Section titled “5. Figma-to-CSS Token Mapping Quick Reference”
Figma PropertyCSS Variable PatternExample
Fill color (primary)var(--color-primary)background-color: var(--color-primary)
Text colorvar(--color-text)color: var(--color-text)
Font family (heading)var(--font-heading)font-family: var(--font-heading)
Font sizevar(--text-{size})font-size: var(--text-2xl)
Spacing / paddingvar(--space-{n})padding: var(--space-8)
Border radiusvar(--radius-{size})border-radius: var(--radius-md)
Box shadowvar(--shadow-{size})box-shadow: var(--shadow-md)
Transitionvar(--transition-{speed})transition: background-color var(--transition-normal)