☰ Categories

Design Handoff Notes - AI First, Human Readable

# Design Handoff Notes — AI-First, Human-Readable ### A structured handoff document optimized for AI implementation agents (Claude Code, Cursor, Copil

CategoryDesign › Design collaboration
TagsDraftingReformattingDeveloperTemplate
Prompt
# Design Handoff Notes — AI-First, Human-Readable

### A structured handoff document optimized for AI implementation agents (Claude Code, Cursor, Copilot) while remaining clear for human developers

---

## About This Prompt

**Description:** Generates a design handoff document that serves as direct implementation instructions for AI coding agents. Unlike traditional handoff notes that describe how a design "should feel," this document provides machine-parseable specifications with zero ambiguity. Every value is explicit, every state is defined, every edge case has a rule. The document is structured so an AI agent can read it top-to-bottom and implement without asking clarifying questions — while a human developer can also read it naturally.

**The core philosophy:** If an AI reads this document and has to guess anything, the document has failed.

**When to use:** After design is finalized, before implementation begins. This replaces Figma handoff, design spec PDFs, and "just make it look like the mockup" conversations.

**Who reads this:**
- Primary: AI coding agents (Claude Code, Cursor, Copilot, etc.)
- Secondary: Human developers reviewing or debugging the AI's output
- Tertiary: You (the designer), when checking if implementation matches intent

**Relationship to CLAUDE.md:** This document assumes a CLAUDE.md design system file already exists in the project root. Handoff Notes reference tokens from CLAUDE.md but don't redefine them. If no CLAUDE.md exists, run the Design System Extraction prompts first.

---

## The Prompt

```
You are a design systems engineer writing implementation specifications.
Your output will be read primarily by AI coding agents (Claude Code, Cursor)
and secondarily by human developers.

Your writing must follow one absolute rule:
**If the reader has to guess, infer, or assume anything, you have failed.**

Every value must be explicit. Every state must be defined. Every edge case
must have a rule. No "as appropriate," no "roughly," no "similar to."

## Project Context
- **Project:** ${name}
- **Framework:** [Next.js 14+ / React / etc.]
- **Styling:** [Tailwind 3.x / CSS Modules / etc.]
- **Component library:** [shadcn/ui / custom / etc.]
- **CLAUDE.md location:** [path — or "not yet created"]
- **Design source:** [uploaded code / live URL / screenshots]
- **Pages to spec:** [all / specific pages]

## Output Format Rules

Before writing any specs, follow these formatting rules exactly:

1. **Values are always code-ready.**
   WRONG: "medium spacing"
   RIGHT: `p-6` (24px)

2. **Colors are always token references + fallback hex.**
   WRONG: "brand blue"
   RIGHT: `text-brand-500` (#2563EB) — from CLAUDE.md tokens

3. **Sizes are always in the project's unit system.**
   If Tailwind: use Tailwind classes as primary, px as annotation
   If CSS: use rem as primary, px as annotation
   WRONG: "make it bigger on desktop"
   RIGHT: `text-lg` (18px) at ≥768px, `text-base` (16px) below

4. **Conditionals use explicit if/else, never "as needed."**
   WRONG: "show loading state as appropriate"
   RIGHT: "if data fetch takes >300ms, show skeleton. If fetch fails, show error state. If data returns empty array, show empty state."

5. **File paths are explicit.**
   WRONG: "create a button component"
   RIGHT: "create `src/components/ui/Button.tsx`"

6. **Every visual property is stated, never inherited by assumption.**
   Even if "obvious" — state it. AI agents don't have visual context.

---

## Document Structure

Generate the handoff document with these sections:

### SECTION 1: IMPLEMENTATION MAP

A priority-ordered table of everything to build.
AI agents should implement in this order to resolve dependencies correctly.

| Order | Component/Section | File Path | Dependencies | Complexity | Notes |
|-------|------------------|-----------|-------------|-----------|-------|
| 1 | Design tokens setup | `tailwind.config.ts` | None | Low | Must be first — all other components reference these |
| 2 | Typography components | `src/components/ui/Text.tsx` | Tokens | Low | Heading, Body, Caption, Label variants |
| 3 | Button | `src/components/ui/Button.tsx` | Tokens, Typography | Medium | 3 variants × 3 sizes × 6 states |
| ... | ... | ... | ... | ... | ... |

Rules:
- Nothing can reference a component that comes later in the table
- Complexity = how many variants × states the component has
- Notes = anything non-obvious about implementation

---

### SECTION 2: GLOBAL SPECIFICATIONS

These apply everywhere. AI agent should configure these BEFORE building any components.

#### 2.1 Breakpoints
Define exact behavior boundaries:

```
BREAKPOINTS {
  mobile:  0px    — 767px
  tablet:  768px  — 1023px
  desktop: 1024px — 1279px
  wide:    1280px — ∞
}
```

For each breakpoint, state:
- Container max-width and padding
- Base font size
- Global spacing multiplier (if it changes)
- Navigation mode (hamburger / horizontal / etc.)

#### 2.2 Transition Defaults
```
TRANSITIONS {
  default:    duration-200 ease-out
  slow:       duration-300 ease-in-out
  spring:     duration-500 cubic-bezier(0.34, 1.56, 0.64, 1)
  none:       duration-0
}

RULE: Every interactive element uses `default` unless
      this document specifies otherwise.
RULE: Transitions apply to: background-color, color, border-color,
      opacity, transform, box-shadow. Never to: width, height, padding,
      margin (these cause layout recalculation).
```

#### 2.3 Z-Index Scale
```
Z-INDEX {
  base:       0
  dropdown:   10
  sticky:     20
  overlay:    30
  modal:      40
  toast:      50
  tooltip:    60
}

RULE: No z-index value outside this scale. Ever.
```

#### 2.4 Focus Style
```
FOCUS {
  style:      ring-2 ring-offset-2 ring-brand-500
  applies-to: every interactive element (buttons, links, inputs, selects, checkboxes)
  visible:    only on keyboard navigation (use focus-visible, not focus)
}
```

---

### SECTION 3: PAGE SPECIFICATIONS

For each page, provide a complete implementation spec.

#### Page: ${page_name}
**Route:** `/exact-route-path`
**Layout:** ${which_layout_wrapper_to_use}
**Data requirements:** [what data this page needs, from where]

##### Page Structure (top to bottom)

```
PAGE STRUCTURE: ${page_name}
├── Section: Hero
│   ├── Component: Heading (h1)
│   ├── Component: Subheading (p)
│   ├── Component: CTA Button (primary, lg)
│   └── Component: HeroImage
├── Section: Features
│   ├── Component: SectionHeading (h2)
│   └── Component: FeatureCard × 3 (grid)
├── Section: Testimonials
│   └── Component: TestimonialSlider
└── Section: CTA
    ├── Component: Heading (h2)
    └── Component: CTA Button (primary, lg)
```

##### Section-by-Section Specs

For each section:

**${section_name}**

```
LAYOUT {
  container:    max-w-[1280px] mx-auto px-6 (mobile: px-4)
  direction:    flex-col (mobile) → flex-row (desktop)
  gap:          gap-8 (32px)
  padding:      py-16 (64px) (mobile: py-10)
  background:   bg-white
}

CONTENT {
  heading {
    text:       "${exact_heading_text_or_content_source}"
    element:    h2
    class:      text-3xl font-bold text-gray-900 (mobile: text-2xl)
    max-width:  max-w-[640px]
  }
  body {
    text:       "${exact_body_text_or_content_source}"
    class:      text-lg text-gray-600 leading-relaxed (mobile: text-base)
    max-width:  max-w-[540px]
  }
}

GRID (if applicable) {
  columns:      grid-cols-3 (tablet: grid-cols-2) (mobile: grid-cols-1)
  gap:          gap-6 (24px)
  items:        ${what_component_renders_in_each_cell}
  alignment:    items-start
}

ANIMATION (if applicable) {
  type:         fade-up on scroll
  trigger:      when section enters viewport (threshold: 0.2)
  stagger:      each child delays 100ms after previous
  duration:     duration-500
  easing:       ease-out
  runs:         once (do not re-trigger on scroll up)
}
```

---

### SECTION 4: COMPONENT SPECIFICATIONS

For each component, provide a complete implementation contract.

#### Component: ${componentname}
**File:** `src/components/${path}/${componentname}.tsx`
**Purpose:** [one sentence — what this component does]

##### Props Interface
```typescript
interface ${componentname}Props {
  variant: 'primary' | 'secondary' | 'ghost'     // visual style
  size: 'sm' | 'md' | 'lg'                        // dimensions
  disabled?: boolean                                // default: false
  loading?: boolean                                 // default: false
  icon?: React.ReactNode                           // optional leading icon
  children: React.ReactNode                         // label content
  onClick?: () => void                              // click handler
}
```

##### Variant × Size Matrix
Define exact values for every combination:

```
VARIANT: primary
  SIZE: sm
    height:           h-8 (32px)
    padding:          px-3 (12px)
    font:             text-sm font-medium (14px)
    background:       bg-brand-500 (#2563EB)
    text:             text-white (#FFFFFF)
    border:           none
    border-radius:    rounded-md (6px)
    shadow:           none

  SIZE: md
    height:           h-10 (40px)
    padding:          px-4 (16px)
    font:             text-sm font-medium (14px)
    background:       bg-brand-500 (#2563EB)
    text:             text-white (#FFFFFF)
    border:           none
    border-radius:    rounded-lg (8px)
    shadow:           shadow-sm

  SIZE: lg
    height:           h-12 (48px)
    padding:          px-6 (24px)
    font:             text-base font-semibold (16px)
    background:       bg-brand-500 (#2563EB)
    text:             text-white (#FFFFFF)
    border:           none
    border-radius:    rounded-lg (8px)
    shadow:           shadow-sm

VARIANT: secondary
  [same structure, different values]

VARIANT: ghost
  [same structure, different values]
```

##### State Specifications
Every state must be defined for every variant:

```
STATES (apply to ALL variants unless overridden):

  hover {
    background:     ${token} — darken one step from default
    transform:      none (no scale/translate on hover)
    shadow:         ${token_or_none}
    cursor:         pointer
    transition:     default (duration-200 ease-out)
  }

  active {
    background:     ${token} — darken two steps from default
    transform:      scale-[0.98]
    transition:     duration-75
  }

  focus-visible {
    ring:           ring-2 ring-offset-2 ring-brand-500
    all other:      same as default state
  }

  disabled {
    opacity:        opacity-50
    cursor:         not-allowed
    pointer-events: none
    ALL hover/active/focus states: do not apply
  }

  loading {
    content:        replace children with spinner (16px, animate-spin)
    width:          maintain same width as non-loading state (prevent layout shift)
    pointer-events: none
    opacity:        opacity-80
  }
```

##### Icon Behavior
```
ICON RULES {
  position:       left of label text (always)
  size:           16px (sm), 16px (md), 20px (lg)
  gap:            gap-1.5 (sm), gap-2 (md), gap-2 (lg)
  color:          inherits text color (currentColor)
  when loading:   icon is hidden, spinner takes its position
  icon-only:      if no children, component becomes square (width = height)
                  add aria-label prop requirement
}
```

---

### SECTION 5: INTERACTION FLOWS

For each user flow, provide step-by-step implementation:

#### Flow: [Flow Name, e.g., "User Signs Up"]
```
TRIGGER:     user clicks "Sign Up" button in header

STEP 1:      Modal opens
             animation:   fade-in (opacity 0→1, duration-200)
             backdrop:    bg-black/50, click-outside closes modal
             focus:       trap focus inside modal, auto-focus first input
             body:        scroll-lock (prevent background scroll)

STEP 2:      User fills form
             fields:      ${list_exact_fields_with_validation_rules}
             validation:  on blur (not on change — reduces noise)
             
             field: email {
               type:       email
               required:   true
               validate:   regex pattern + "must contain @ and domain"
               error:      "That doesn't look like an email — check for typos"
               success:    green checkmark icon appears (fade-in, duration-150)
             }
             
             field: password {
               type:       password (with show/hide toggle)
               required:   true
               validate:   min 8 chars, 1 uppercase, 1 number
               error:      show checklist of requirements, highlight unmet
               strength:   show strength bar (weak/medium/strong)
             }

STEP 3:      User submits
             button:      shows loading state (see Button component spec)
             request:     POST /api/auth/signup
             duration:    expect 1-3 seconds

STEP 4a:     Success
             modal:       content transitions to success message (crossfade, duration-200)
             message:     "Account created! Check your email to verify."
             action:      "Got it" button closes modal
             redirect:    after close, redirect to /dashboard
             toast:       none (the modal IS the confirmation)

STEP 4b:     Error — email exists
             field:       email input shows error state
             message:     "This email already has an account — want to log in instead?"
             action:      "Log in" link switches modal to login form
             button:      returns to default state (not loading)

STEP 4c:     Error — network failure
             display:     error banner at top of modal (not a toast)
             message:     "Something went wrong on our end. Try again?"
             action:      "Try again" button re-submits
             button:      returns to default state

STEP 4d:     Error — rate limited
             display:     error banner
             message:     "Too many attempts. Wait 60 seconds and try again."
             button:      disabled for 60 seconds with countdown visible
```

---

### SECTION 6: RESPONSIVE BEHAVIOR RULES

Don't describe what changes — specify the exact rules:

```
RESPONSIVE RULES:

Rule 1: Navigation
  ≥1024px:    horizontal nav, all items visible
  <1024px:    hamburger icon, slide-in drawer from right
              drawer-width: 80vw (max-w-[320px])
              animation: translate-x (duration-300 ease-out)
              backdrop: bg-black/50, click-outside closes

Rule 2: Grid Sections
  ≥1024px:    grid-cols-3
  768-1023px: grid-cols-2 (last item spans full if odd count)
  <768px:     grid-cols-1

Rule 3: Hero Section
  ≥1024px:    two-column (text left, image right) — 55/45 split
  <1024px:    single column (text top, image bottom)
              image max-height: 400px, object-cover

Rule 4: Typography Scaling
  ≥1024px:    h1=text-5xl, h2=text-3xl, h3=text-xl, body=text-base
  <1024px:    h1=text-3xl, h2=text-2xl, h3=text-lg, body=text-base

Rule 5: Spacing Scaling
  ≥1024px:    section-padding: py-16, container-padding: px-8
  768-1023px: section-padding: py-12, container-padding: px-6
  <768px:     section-padding: py-10, container-padding: px-4

Rule 6: Touch Targets
  <1024px:    all interactive elements minimum 44×44px hit area
              if visual size < 44px, use invisible padding to reach 44px

Rule 7: Images
  all images: use next/image with responsive sizes prop
  hero:       sizes="(max-width: 1024px) 100vw, 50vw"
  grid items: sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
```

---

### SECTION 7: EDGE CASES & BOUNDARY CONDITIONS

This section prevents the "but what happens when..." problems:

```
EDGE CASES:

Text Overflow {
  headings:     max 2 lines, then truncate with text-ellipsis (add title attr for full text)
  body text:    allow natural wrapping, no truncation
  button labels: single line only, max 30 characters, no truncation (design constraint)
  nav items:    single line, truncate if >16 characters on mobile
  table cells:  truncate with tooltip on hover
}

Empty States {
  lists/grids with 0 items: show ${emptystate} component
    - illustration: ${describe_or_reference_asset}
    - heading: "${exact_text}"
    - body: "${exact_text}"
    - CTA: "${exact_text}" → ${action}
  
  user avatar missing: show initials on colored background
    - background: generate from user name hash (deterministic)
    - initials: first letter of first + last name, uppercase
    - font: text-sm font-medium text-white
  
  image fails to load: show gray placeholder with image icon
    - background: bg-gray-100
    - icon: ImageOff from lucide-react, text-gray-400, 24px
}

Loading States {
  page load:      full-page skeleton (not spinner)
  component load: component-level skeleton matching final dimensions
  button action:  inline spinner in button (see Button spec)
  infinite list:  skeleton row × 3 at bottom while fetching next page
  
  skeleton style: bg-gray-200 rounded animate-pulse
  skeleton rule:  skeleton shape must match final content shape
                  (rectangle for text, circle for avatars, rounded-lg for cards)
}

Error States {
  API error (500):     show inline error banner with retry button
  Network error:       show "You seem offline" banner at top (auto-dismiss when reconnected)
  404 content:         show custom 404 component (not Next.js default)
  Permission denied:   redirect to /login with return URL param
  Form validation:     inline per-field (see flow specs), never alert()
}

Data Extremes {
  username 1 character:   display normally
  username 50 characters: truncate at 20 in nav, full in profile
  price $0.00:            show "Free"
  price $999,999.99:      ensure layout doesn't break (test with formatted number)
  list with 1 item:       same layout as multiple (no special case)
  list with 500 items:    paginate at 20, show "Load more" button
  date today:             show "Today" not the date
  date this year:         show "Mar 13" not "Mar 13, 2026"
  date other year:        show "Mar 13, 2025"
}
```

---

### SECTION 8: IMPLEMENTATION VERIFICATION CHECKLIST

After implementation, the AI agent (or human developer) should verify:

```
VERIFICATION:

□ Every component matches the variant × size matrix exactly
□ Every state (hover, active, focus, disabled, loading) works
□ Tab order follows visual order on all pages
□ Focus-visible ring appears on keyboard nav, not on mouse click
□ All transitions use specified duration and easing (not browser default)
□ No layout shift during page load (check CLS)
□ Skeleton states match final content dimensions
□ All edge cases from Section 7 are handled
□ Touch targets ≥ 44×44px on mobile breakpoints
□ No horizontal scroll at any breakpoint
□ All images use next/image with correct sizes prop
□ Z-index values only use the defined scale
□ Error states display correctly (test with network throttle)
□ Empty states display correctly (test with empty data)
□ Text truncation works at boundary lengths
□ Dark mode tokens (if applicable) are all mapped
```

---

## How the AI Agent Should Use This Document

Include this instruction at the top of the generated handoff document
so the implementing AI knows how to work with it:

```
INSTRUCTIONS FOR AI IMPLEMENTATION AGENT:

1. Read this document fully before writing any code.
2. Implement in the order specified in SECTION 1 (Implementation Map).
3. Reference CLAUDE.md for token values. If a token referenced here
   is not in CLAUDE.md, flag it and use the fallback value provided.
4. Every value in this document is intentional. Do not substitute
   with "close enough" values. `gap-6` means `gap-6`, not `gap-5`.
5. Every state must be implemented. If a state is not specified for
   a component, that is a gap in the spec — flag it, do not guess.
6. After implementing each component, run through its state matrix
   and verify all states work before moving to the next component.
7. When encountering ambiguity, prefer the more explicit interpretation.
   If still ambiguous, add a TODO comment: "// HANDOFF-AMBIGUITY: [description]"
```
```

---

## Customization Notes

**If you're not using Tailwind:** Replace all Tailwind class references in the prompt with your system's equivalents. The structure stays the same — only the value format changes. Tell Claude: "Use CSS custom properties as primary, px values as annotations."

**If you're handing off to a specific AI tool:** Add tool-specific notes. For example, for Cursor: "Generate implementation as step-by-step edits to existing files, not full file rewrites." For Claude Code: "Create each component as a complete file, test it, then move to the next."

**If no CLAUDE.md exists yet:** Tell the prompt to generate a minimal token section at the top of the handoff document covering only the tokens needed for this specific handoff. It won't be a full design system, but it prevents hardcoded values.

**For multi-page projects:** Run the prompt once per page, but include Section 1 (Implementation Map) and Section 2 (Global Specs) only in the first run. Subsequent pages reference the same globals.

What this prompt does

This prompt generates handoff specs after design is finalized and before implementation. It requires every value, state, and edge case to be explicit, assuming CLAUDE.md tokens may be referenced.

Model comparison

ChatGPT and Gemini are well structured but invent a Moodystore spec from an unfilled template. ChatGPT is more complete, Gemini is truncated, and [C] is missing.

ChatGPTTop overall
23/ 50

+ Clear build order and state specifications.

- It invents missing project and design details.

Gemini
22/ 50

+ Responsive layouts and contracts are clear.

- It fabricates the spec and ends mid-sentence.

CriterionChatGPTGeminiLeader
Instruction following33Tie
Accuracy22Tie
Specificity66Tie
Structure88Tie
Right length43ChatGPT +33%

Scored 1–10 by gpt-5.6-sol with model names hidden (2026-09-25). This is an AI review, not a measurement.

Read full answers

We gave three models the same input and copied their answers unedited. Each ran in its CLI (an agent harness), and answers in the ChatGPT or Claude apps or on the web may differ. Outputs are in Korean.

Input used
[Pasted material]
Project: 무디스토어 (라이프스타일 편집숍 온라인 스토어)
Framework: Next.js 14 (App Router)
Styling: Tailwind CSS 3.4
Component library: shadcn/ui
CLAUDE.md location: /Users/park/project/moodystore/CLAUDE.md (이미 존재, 색상·타이포 토큰 정의됨)
Design source: Figma에서 export한 스크린샷 3장 업로드 (홈, 상품 상세, 장바구니)
Pages to spec: 상품 상세 페이지(Product Detail Page) 1개만 우선 작성 — Section 3의 ${page_name} 자리에는 '상품 상세 페이지'를 넣어서 작성해줘
gpt-5.6-luna (codex CLI · 에이전트 하네스) · Run on 2026-09-24

무디스토어 상품 상세 페이지 디자인 핸드오프

문서 사용 지침

  1. 이 문서를 전체 읽은 후 Section 1 순서대로 구현한다.
  2. 스타일 값은 /Users/park/project/moodystore/CLAUDE.md의 토큰을 우선 사용한다.
  3. 토큰이 없으면 이 문서의 fallback 값을 사용하고 // HANDOFF-AMBIGUITY 주석을 남긴다.
  4. 명시된 Tailwind 클래스, 상태, 중단점, 파일 경로를 임의로 변경하지 않는다.
  5. 모든 컴포넌트 구현 후 hover, active, focus-visible, disabled, loading 상태를 확인한다.
  6. 실제 상품 데이터가 없을 때는 아래 명시된 fixture 데이터를 사용한다.
  7. 상품 상세 페이지 외 페이지는 이번 문서의 구현 범위에 포함하지 않는다.

SECTION 1: IMPLEMENTATION MAP

순서 컴포넌트/섹션 파일 경로 의존성 복잡도 구현 규칙
1 상품 타입 및 fixture src/types/product.ts, src/data/product-fixture.ts 없음 Low 상품, 옵션, 이미지, 리뷰 타입을 정의한다.
2 상품 상세 데이터 로더 src/lib/products/get-product.ts 상품 타입 Medium /products/[slug]에서 상품을 조회한다.
3 전역 상품 상세 레이아웃 src/components/product/ProductDetailLayout.tsx 상품 타입 Medium 데스크톱 2열, 모바일 1열 레이아웃을 담당한다.
4 상품 이미지 갤러리 src/components/product/ProductGallery.tsx Button, 상품 타입 Medium 썸네일 선택, 확대, 이미지 오류 상태를 처리한다.
5 상품 정보 영역 src/components/product/ProductInfo.tsx Text, Badge Medium 브랜드, 상품명, 가격, 리뷰 요약을 표시한다.
6 옵션 선택기 src/components/product/ProductOptionSelector.tsx Button High 옵션 선택, 품절, 선택 오류를 처리한다.
7 수량 선택기 src/components/product/QuantitySelector.tsx Button Medium 최소 1개, 최대 재고 수량을 적용한다.
8 구매 액션 영역 src/components/product/PurchaseActions.tsx Button, QuantitySelector High 장바구니, 바로 구매, 찜을 처리한다.
9 상품 상세 탭 src/components/product/ProductDetailTabs.tsx Tabs Medium 상세 정보, 리뷰, 배송/교환 탭을 제공한다.
10 추천 상품 목록 src/components/product/RelatedProducts.tsx ProductCard Medium 같은 카테고리 상품을 표시한다.
11 상품 상세 페이지 src/app/products/[slug]/page.tsx 1–10 전체 High 데이터 조회와 페이지 조합을 담당한다.
12 로딩/오류/빈 상태 src/app/products/[slug]/loading.tsx, error.tsx, not-found.tsx Skeleton, ErrorBanner Medium Next.js App Router 상태 파일을 사용한다.

SECTION 2: GLOBAL SPECIFICATIONS

2.1 Breakpoints

BREAKPOINTS {
  mobile:  0px    — 767px
  tablet:  768px  — 1023px
  desktop: 1024px — 1279px
  wide:    1280px — ∞
}
구간 컨테이너 좌우 패딩 기본 폰트 헤더
mobile max-w-none px-4 (16px) text-base (16px) 메뉴 버튼
tablet max-w-[960px] px-6 (24px) text-base (16px) 축약 헤더
desktop max-w-[1200px] px-8 (32px) text-base (16px) 전체 헤더
wide max-w-[1280px] px-8 (32px) text-base (16px) 전체 헤더

상품 상세 콘텐츠의 최대 너비는 max-w-[1280px] (1280px)로 제한한다.

2.2 전환

TRANSITIONS {
  default: duration-200 ease-out
  slow:    duration-300 ease-in-out
  spring:  duration-500 cubic-bezier(0.34, 1.56, 0.64, 1)
  none:    duration-0
}

모든 인터랙티브 요소는 별도 지정이 없으면 default를 사용한다.

전환 대상:

  • background-color
  • color
  • border-color
  • opacity
  • transform
  • box-shadow

width, height, padding, margin에는 transition을 적용하지 않는다.

2.3 Z-Index

Z-INDEX {
  base:     0
  dropdown: 10
  sticky:   20
  overlay:  30
  modal:    40
  toast:    50
  tooltip:  60
}

이 외의 z-index 값은 사용하지 않는다.

2.4 Focus

FOCUS {
  style: ring-2 ring-offset-2 ring-brand-500
  applies-to: button, link, input, select, checkbox, tab
  visible: focus-visible only
}

CLAUDE.md에 정의된 브랜드 포커스 토큰이 있으면 해당 토큰을 사용한다.

2.5 기본 토큰 fallback

용도 토큰 Fallback
페이지 배경 bg-background #FFFFFF
기본 텍스트 text-foreground #171717
보조 텍스트 text-muted-foreground #737373
테두리 border-border #E5E5E5
주요 브랜드 색상 brand-500 #6B4F3A
비활성 배경 bg-muted #F5F5F5
오류 text-destructive #DC2626
성공 text-success #16A34A

SECTION 3: PAGE SPECIFICATIONS

상품 상세 페이지

Route: /products/[slug]

예시 URL: /products/linen-cup

Layout: ProductDetailLayout

렌더링 방식: Server Component 기본. 옵션 선택, 갤러리, 수량, 장바구니 액션이 필요한 컴포넌트만 Client Component로 분리한다.

Data requirements:

interface Product {
  id: string
  slug: string
  brandName: string
  name: string
  shortDescription: string
  price: number
  originalPrice?: number
  discountRate?: number
  currency: 'KRW'
  images: ProductImage[]
  options: ProductOption[]
  stock: number
  rating: number
  reviewCount: number
  descriptionHtml: string
  shipping: ShippingInfo
  category: string
  relatedProductIds: string[]
}

상품 데이터 조회 실패 시 notFound()를 호출한다.

페이지 구조

PAGE STRUCTURE: 상품 상세 페이지
├── SiteHeader
├── Breadcrumb
├── ProductDetailLayout
│   ├── ProductGallery
│   │   ├── MainProductImage
│   │   └── ThumbnailList
│   └── ProductInfo
│       ├── BrandName
│       ├── ProductName
│       ├── RatingSummary
│       ├── PriceBlock
│       ├── ShortDescription
│       ├── ProductOptionSelector
│       ├── QuantitySelector
│       └── PurchaseActions
├── ProductDetailTabs
│   ├── DescriptionTab
│   ├── ReviewTab
│   └── ShippingTab
├── RelatedProducts
└── SiteFooter

3.1 Breadcrumb

LAYOUT {
  container: max-w-[1280px] mx-auto px-4 md:px-6 lg:px-8
  height: h-12 (48px)
  display: flex
  align-items: items-center
  gap: gap-2 (8px)
  font: text-sm (14px)
  color: text-muted-foreground (#737373)
}

표시 순서:

홈 / 카테고리명 / 상품명

마지막 상품명은 text-foreground로 표시하며 링크로 만들지 않는다.

상품명이 32자를 초과하면 한 줄에서 truncate하고 title 속성에 전체 상품명을 넣는다.

3.2 상품 상세 본문

LAYOUT {
  container: max-w-[1280px] mx-auto px-4 md:px-6 lg:px-8
  display: grid
  columns: grid-cols-1 lg:grid-cols-2
  gap: gap-8 md:gap-10 lg:gap-16
  padding: pb-16 md:pb-20 lg:pb-24
}

데스크톱에서 이미지 영역과 정보 영역은 각각 min-w-0를 적용한다.

  • 이미지 영역: lg:col-span-1
  • 정보 영역: lg:col-span-1
  • 데스크톱 정보 영역 최대 너비: max-w-[520px]
  • 모바일 정보 영역: 이미지 아래에 배치
  • 태블릿: 이미지와 정보가 세로로 배치

3.3 상품 이미지 갤러리

이미지 영역

MAIN IMAGE {
  aspect-ratio: aspect-square
  width: w-full
  background: bg-muted (#F5F5F5)
  border-radius: rounded-lg (8px)
  object-fit: object-cover
}

첫 번째 이미지를 초기 선택 상태로 사용한다.

이미지에는 다음 sizes를 사용한다.

sizes="(max-width: 1023px) 100vw, 50vw"

썸네일 목록

THUMBNAILS {
  display: flex
  gap: gap-2 (8px)
  margin-top: mt-3 (12px)
  overflow: overflow-x-auto
}

각 썸네일:

width: w-20 (80px) mobile
width: w-24 (96px) desktop
height: 동일 width
border-radius: rounded-md (6px)
object-fit: object-cover

선택된 썸네일:

border: border-2 border-brand-500
opacity: opacity-100

선택되지 않은 썸네일:

border: border border-border
opacity: opacity-60
hover: opacity-100

이미지 개수가 1개이면 썸네일 목록을 렌더링하지 않는다.

이미지 로딩 실패 시:

background: bg-gray-100 (#F3F4F6)
icon: ImageOff, 24px, text-gray-400
aria-label: "상품 이미지를 불러올 수 없습니다"

3.4 상품 정보

브랜드명

element: p
class: text-sm font-medium text-muted-foreground
margin-bottom: mb-2

상품명

element: h1
desktop: text-3xl font-semibold leading-tight
mobile: text-2xl font-semibold leading-tight
color: text-foreground
max-lines: 2
overflow: line-clamp-2

전체 상품명은 title 속성에 저장한다.

리뷰 요약

display: flex
align-items: items-center
gap: gap-2
margin-top: mt-4
font: text-sm

구성:

별점: ★ 5개
별점 숫자: 4.8
리뷰 링크: 리뷰 24개

별점은 0.5 단위로 표시한다. 리뷰가 0개이면 별점 대신 다음 문구를 표시한다.

"첫 리뷰를 남겨보세요"

가격

margin-top: mt-6

할인 가격이 있는 경우:

original price:
  text-sm line-through text-muted-foreground

discount rate:
  text-lg font-semibold text-destructive

current price:
  text-2xl font-bold text-foreground

할인 가격이 없는 경우 현재 가격만 표시한다.

가격 포맷:

new Intl.NumberFormat('ko-KR').format(price) + '원'

가격이 0이면 0원 대신 무료를 표시한다.

짧은 설명

element: p
margin-top: mt-4
font: text-base leading-7
color: text-muted-foreground
white-space: pre-line

3.5 옵션 선택

옵션이 존재하면 가격 블록 아래 mt-8에 표시한다.

옵션이 없는 상품은 옵션 선택기를 렌더링하지 않는다.

OPTION SELECTOR {
  label: text-sm font-medium text-foreground
  label-margin: mb-2
  trigger: h-11 w-full rounded-md border border-border px-3
}

옵션 상태:

상태 스타일
기본 border-border bg-background
hover border-foreground
focus-visible ring-2 ring-offset-2 ring-brand-500
선택 완료 border-foreground
품절 text-muted-foreground line-through, 선택 불가
오류 border-destructive, 오류 문구 표시

옵션 선택 없이 구매 버튼을 클릭하면:

error message: "옵션을 선택해 주세요."
placement: 옵션 선택기 바로 아래
color: text-destructive
font: text-sm

3.6 수량 선택

display: flex
align-items: center
width: w-fit
height: h-11 (44px)
border: border border-border
border-radius: rounded-md
margin-top: mt-6

구성:

감소 버튼: 44×44px
수량 표시: min-width 48px, text-center
증가 버튼: 44×44px

규칙:

  • 초기 수량: 1
  • 최소 수량: 1
  • 최대 수량: stock
  • 수량이 1이면 감소 버튼 disabled
  • 수량이 재고 수량과 같으면 증가 버튼 disabled
  • 재고가 0이면 수량 선택기와 구매 버튼을 disabled 처리

3.7 구매 액션

PURCHASE ACTIONS {
  display: grid
  columns: grid-cols-[auto_1fr_1fr]
  gap: gap-2
  margin-top: mt-6
}

모바일에서는 다음과 같이 변경한다.

columns: grid-cols-1

버튼 순서:

  1. 찜하기
  2. 장바구니
  3. 바로 구매

찜하기 버튼:

size: h-12 w-12
variant: outline
icon: Heart, 20px
aria-label: "찜하기"

장바구니 버튼:

height: h-12 (48px)
variant: secondary
text: "장바구니"

바로 구매 버튼:

height: h-12 (48px)
variant: primary
text: "바로 구매"

장바구니 성공 시:

toast message: "장바구니에 상품을 담았습니다."
toast duration: 3000ms

바로 구매 성공 시:

redirect: "/checkout"

SECTION 4: COMPONENT SPECIFICATIONS

Component: ProductGallery

File: src/components/product/ProductGallery.tsx

Purpose: 상품 이미지 목록을 표시하고 선택된 이미지를 변경한다.

interface ProductGalleryProps {
  images: ProductImage[]
  productName: string
}

상태:

initial selectedIndex: 0
image loading: skeleton matching aspect-square
image error: bg-gray-100 + ImageOff icon

키보드 동작:

  • 썸네일 Tab: 다음 썸네일로 이동
  • Enter 또는 Space: 썸네일 선택
  • 선택된 썸네일: aria-current="true"

Component: ProductOptionSelector

File: src/components/product/ProductOptionSelector.tsx

interface ProductOptionSelectorProps {
  options: ProductOption[]
  value?: string
  onChange: (optionId: string) => void
  error?: string
  disabled?: boolean
}

규칙:

  • disabled=true이면 전체 옵션 선택 불가
  • 품절 옵션은 aria-disabled="true"를 사용한다.
  • 오류 문구는 aria-describedby로 연결한다.
  • 옵션 선택 전 구매 시 오류를 표시한다.
  • 옵션이 하나이면 자동 선택하지 않는다. 사용자가 명시적으로 선택해야 한다.

Component: QuantitySelector

File: src/components/product/QuantitySelector.tsx

interface QuantitySelectorProps {
  value: number
  min?: number
  max: number
  onChange: (value: number) => void
  disabled?: boolean
}

기본값:

min: 1
max: product.stock

수량 변경은 정수만 허용한다. value < min이면 min, value > max이면 max로 보정한다.

Component: PurchaseActions

File: src/components/product/PurchaseActions.tsx

interface PurchaseActionsProps {
  productId: string
  quantity: number
  optionId?: string
  stock: number
  isWishlisted: boolean
  onWishlist: () => void
  onAddToCart: () => Promise<void>
  onBuyNow: () => Promise<void>
  disabled?: boolean
}

상태

default:
  버튼 활성화

loading:
  클릭한 액션 버튼 내부에 16px spinner
  버튼 텍스트 유지
  해당 액션 pointer-events-none
  중복 요청 방지

success:
  장바구니: toast 표시
  바로 구매: /checkout 이동

error:
  버튼 loading 해제
  inline error banner 표시
  문구: "처리 중 문제가 발생했습니다. 다시 시도해 주세요."

Component: ProductDetailTabs

File: src/components/product/ProductDetailTabs.tsx

interface ProductDetailTabsProps {
  descriptionHtml: string
  reviews: Review[]
  shipping: ShippingInfo
}

탭:

"상품 설명"
"리뷰"
"배송 및 교환"

레이아웃:

container: max-w-[1280px] mx-auto px-4 md:px-6 lg:px-8
border-top: border-t border-border
tab-list: flex overflow-x-auto
tab-height: h-14 (56px)

활성 탭:

border-bottom: border-2 border-foreground
font: font-semibold
color: text-foreground

비활성 탭:

color: text-muted-foreground

탭 패널은 활성 탭 하나만 렌더링한다.


SECTION 5: INTERACTION FLOWS

Flow: 상품 옵션 선택 후 장바구니 추가

TRIGGER:
  사용자가 "장바구니" 버튼 클릭

STEP 1:
  옵션이 존재하고 선택되지 않았으면
  옵션 선택기 아래에 "옵션을 선택해 주세요." 표시
  포커스를 첫 번째 미선택 옵션으로 이동
  요청하지 않음

STEP 2:
  옵션이 선택되었으면
  선택한 버튼을 loading 상태로 변경
  중복 클릭 차단
  POST /api/cart/items 요청

STEP 3a:
  요청 성공
  버튼 loading 해제
  "장바구니에 상품을 담았습니다." toast 표시
  toast duration: 3000ms

STEP 3b:
  재고 부족
  버튼 loading 해제
  오류 문구: "현재 재고보다 많은 수량을 선택했습니다."
  수량을 현재 재고로 보정

STEP 3c:
  네트워크 오류
  버튼 loading 해제
  오류 문구: "네트워크 연결을 확인한 후 다시 시도해 주세요."
  "다시 시도" 액션 표시

Flow: 바로 구매

TRIGGER:
  사용자가 "바로 구매" 버튼 클릭

IF:
  필수 옵션 미선택
  → 옵션 오류 표시
  → 구매 요청 중단

IF:
  재고가 0
  → "품절된 상품입니다." 표시
  → 버튼 disabled

ELSE:
  버튼 loading
  POST /api/checkout/prepare
  성공 시 /checkout으로 이동
  실패 시 오류 배너 표시

Flow: 이미지 변경

TRIGGER:
  사용자가 썸네일 클릭

ACTION:
  selectedIndex 변경
  메인 이미지 opacity 0→1
  transition: duration-200 ease-out
  URL 변경 없음
  페이지 스크롤 위치 유지

Flow: 찜하기

IF:
  로그인 상태
  → POST /api/wishlist
  → Heart fill 상태로 변경

IF:
  비로그인 상태
  → /login?returnUrl=/products/[slug]로 이동

요청 실패:
  → "찜하기를 완료하지 못했습니다." 오류 표시
  → 기존 상태 유지

SECTION 6: RESPONSIVE BEHAVIOR RULES

Rule 1: 상품 본문 레이아웃

≥1024px:
  grid-cols-2
  image/info gap: gap-16
  이미지와 정보가 같은 행에 배치

768–1023px:
  grid-cols-1
  이미지가 위, 정보가 아래
  gap: gap-10

<768px:
  grid-cols-1
  gap: gap-8
  container padding: px-4

Rule 2: 상품 이미지

≥1024px:
  메인 이미지 aspect-square
  썸네일 96×96px

<1024px:
  메인 이미지 aspect-square
  썸네일 80×80px
  썸네일 목록 가로 스크롤 허용

Rule 3: 구매 버튼

≥768px:
  찜하기, 장바구니, 바로 구매를 한 줄에 표시

<768px:
  세 버튼을 세로로 표시
  각 버튼 최소 높이 48px

Rule 4: 상세 탭

모든 구간:
  탭 목록 가로 스크롤 가능
  탭 라벨 줄바꿈 금지
  white-space: nowrap

Rule 5: 터치 영역

<1024px:
  모든 버튼과 썸네일의 실제 hit area 최소 44×44px
  시각적 아이콘이 20px이어도 버튼 영역은 44px 유지

Rule 6: 이미지 sizes

상품 메인 이미지:
sizes="(max-width: 1023px) 100vw, 50vw"

추천 상품 이미지:
sizes="(max-width: 767px) 50vw, (max-width: 1023px) 33vw, 25vw"

SECTION 7: EDGE CASES & BOUNDARY CONDITIONS

Text Overflow

상품명:
  최대 2줄
  line-clamp-2
  전체 텍스트는 title 속성에 저장

브랜드명:
  최대 1줄
  truncate

옵션명:
  최대 1줄
  truncate
  title 속성에 전체 텍스트 저장

가격:
  999,999,999원까지 한 줄 유지
  숫자 영역에 whitespace-nowrap 적용

상품 설명:
  자연 줄바꿈 허용
  truncate하지 않음

Empty States

리뷰 0개:
  heading: "아직 리뷰가 없습니다"
  body: "이 상품을 처음 구매한 고객이 되어 리뷰를 남겨보세요."
  CTA: 상품 구매 후 리뷰 작성

관련 상품 0개:
  관련 상품 섹션을 렌더링하지 않음

상품 이미지 0개:
  bg-gray-100
  ImageOff 아이콘 24px
  aria-label: "상품 이미지가 없습니다"

Loading States

페이지 로딩:
  ProductDetailSkeleton 표시
  메인 이미지: aspect-square skeleton
  상품명: h-8 w-3/4 skeleton
  가격: h-8 w-1/3 skeleton
  옵션: h-11 w-full skeleton
  액션 버튼: h-12 w-full skeleton

상품 이미지:
  bg-gray-200 rounded-lg animate-pulse

버튼 요청:
  버튼 내부 16px animate-spin spinner
  버튼 폭 유지

Error States

상품 없음:
  not-found.tsx 렌더링
  heading: "상품을 찾을 수 없습니다"
  CTA: "상품 목록으로 돌아가기"
  action: /products

API 오류:
  inline error banner
  message: "상품 정보를 불러오지 못했습니다."
  action: "다시 시도"

네트워크 오류:
  message: "네트워크 연결을 확인해 주세요."
  auto-dismiss: 연결 복구 시

재고 0:
  price 아래에 "품절" 표시
  수량 선택기 disabled
  장바구니/바로 구매 disabled

Data Extremes

상품명 1자:
  그대로 표시

상품명 100자:
  h1 최대 2줄로 표시하고 나머지 truncate

가격 0:
  "무료" 표시

가격 999,999,999:
  줄바꿈 금지
  컨테이너 overflow 허용하지 않음

옵션 1개:
  선택기를 표시하고 자동 선택하지 않음

옵션 20개:
  옵션 목록 최대 높이 320px
  overflow-y-auto

리뷰 0개:
  빈 리뷰 상태 표시

리뷰 500개:
  초기 10개 표시
  "리뷰 더보기" 버튼으로 10개씩 추가

재고 0:
  구매 액션 전체 disabled

재고 999:
  수량 증가 버튼은 재고 999에서 disabled

접근성

상품 이미지:
  alt="${productName} 상품 이미지"

장식용 이미지:
  alt=""

썸네일:
  button 요소 사용
  aria-label="${productName} 이미지 ${index} 보기"

탭:
  role="tablist", role="tab", role="tabpanel"
  aria-selected 사용

옵션 오류:
  aria-invalid="true"
  aria-describedby 연결

모든 아이콘 전용 버튼:
  aria-label 필수

SECTION 8: IMPLEMENTATION VERIFICATION CHECKLIST

□ /products/[slug] 라우트가 정상 렌더링된다
□ 상품 데이터가 없으면 not-found.tsx가 표시된다
□ 모바일에서 상품 이미지가 정보 영역 위에 배치된다
□ 데스크톱에서 이미지와 정보가 2열로 배치된다
□ 메인 이미지와 썸네일 선택 상태가 일치한다
□ 이미지 로딩 실패 시 ImageOff placeholder가 표시된다
□ 상품명이 2줄을 초과하지 않는다
□ 가격이 한국 원화 형식으로 표시된다
□ 가격 0원은 "무료"로 표시된다
□ 할인 전 가격, 할인율, 현재 가격이 올바르게 표시된다
□ 옵션이 없을 때 옵션 선택기가 렌더링되지 않는다
□ 옵션 미선택 상태에서 구매할 수 없다
□ 품절 옵션을 선택할 수 없다
□ 수량 최소값은 1이다
□ 수량 최대값은 재고 수량이다
□ 재고 0이면 구매 관련 버튼이 disabled다
□ 장바구니 요청 중 버튼이 loading 상태다
□ 중복 장바구니 요청이 발생하지 않는다
□ 장바구니 성공 toast가 3초 표시된다
□ 바로 구매 성공 시 `/checkout`으로 이동한다
□ 비로그인 찜하기 시 return URL이 유지된다
□ 리뷰 0개 상태가 정상 표시된다
□ 관련 상품 0개일 때 섹션이 숨겨진다
□ 상세 탭 키보드 탐색이 가능하다
□ 모든 focus-visible 링이 표시된다
□ 터치 대상이 44×44px 이상이다
□ 767px, 768px, 1023px, 1024px, 1279px, 1280px에서 레이아웃을 확인했다
□ 가로 스크롤이 의도된 썸네일/탭 영역 외에는 발생하지 않는다
□ 모든 이미지를 `next/image`로 렌더링한다
□ 메인 이미지 `sizes`가 지정되어 있다
□ 추천 상품 이미지 `sizes`가 지정되어 있다
□ z-index가 정의된 scale 외의 값을 사용하지 않는다
□ 로딩 skeleton이 최종 콘텐츠 크기와 일치한다
□ 네트워크 오류와 API 오류가 구분되어 표시된다
□ 상품명, 옵션명, 가격의 경계값을 테스트했다
□ `prefers-reduced-motion`에서 애니메이션이 제거된다

실제 Figma 스크린샷에서 확인되지 않은 텍스트·상품 콘텐츠는 CMS/API 데이터로 주입한다. 시각적 토큰은 CLAUDE.md를 최우선으로 적용한다.

More in this category

Design Brief
Work out what a change request actually costs
Read the decisions behind a competitor's screen
Decode vague design feedback
Write a developer handoff spec