# Microsoft Fluent 2 디자인 시스템

## Microsoft Fluent 2 디자인 시스템 인터페이스 미리보기

Brief와 People 간에 전환하고, 웹 테마를 켜고 끄며, 잘못된 입력·중복 입력·로컬 추가 성공 사례를 확인해 보세요. 초대는 전송되지 않습니다.

## Microsoft Fluent 2 디자인 시스템 디자인 참고 자료

Fluent UI React v9 웹 참조 자료입니다. 전역 숫자 값은 연결된 React 토큰 소스에서 가져옵니다. 아래 색상 값은 brandWeb와 라이트 별칭 매핑을 기준으로 확인했으며, alias/lightColor.ts에 남은 오래된 예시용 16진수 주석은 기준으로 삼지 않았습니다. 다크 색상과 Teams 테마에는 별도의 매핑이 적용됩니다. 웹사이트의 타이포그래피 표에는 현재 Subtitle1이 20/26으로 나와 있지만, React fonts.ts 소스는 20/28을 사용하므로 이 구현에서는 소스 값을 따릅니다. 2026년 10월 3일에 소스를 확인했습니다. lockfile에 기록된 버전과 대조하세요.

### 라이트 테마의 중립 색상 역할

| 이름 / 규칙 | 값 / 구성 | 용도 |
| --- | --- | --- |
| colorNeutralForeground1 | #242424 | 기본 문서 텍스트입니다. |
| colorNeutralForeground2 | #424242 | 보조 레이블. |
| colorNeutralForeground3 | #616161 | 메타데이터입니다. 최종 대비도 확인하세요. |
| colorNeutralBackground1 | #ffffff | 기본 표면입니다. |
| colorNeutralBackground2 | #fafafa | 차분한 보조 표면입니다. |
| colorNeutralBackground3 | #f5f5f5 | 작업 공간 캔버스입니다. |
| colorNeutralStroke1 | #d1d1d1 | 일반적인 컴포넌트 경계입니다. |
| colorNeutralStroke2 | #e0e0e0 | 은은한 구분선입니다. |
| colorNeutralStrokeAccessible | #616161 | 대비가 더 높은 컨트롤 경계입니다. |
| colorNeutralForegroundOnBrand | #ffffff | 브랜드 배경색 위에 표시되는 텍스트입니다. |
| colorStrokeFocus1 / colorStrokeFocus2 | #ffffff / #000000 | 포커스 색상 두 가지입니다. 컴포넌트 가장자리에서 대비가 유지되도록 하세요. |
### brandWeb에서 확인한 웹 브랜드 색상 역할

| 이름 / 규칙 | 값 / 구성 | 용도 |
| --- | --- | --- |
| colorBrandBackground | #0f6cbd · brandWeb[80] | webLightTheme의 기본 작업입니다. |
| colorBrandBackgroundHover | #115ea3 · brandWeb[70] | 해당 작업의 호버 상태입니다. |
| colorBrandBackgroundPressed | #0c3b5e · brandWeb[40] | 눌림 상태입니다. 일반적인 선택 상태를 뜻하지는 않습니다. |
| colorBrandBackground2 | #ebf3fc · brandWeb[160] | 은은한 브랜드 배경입니다. |
| colorBrandForegroundLink | #115ea3 · brandWeb[70] | 라이트 웹 테마의 링크 전경색입니다. |
### 가로 및 세로 간격

| 이름 / 규칙 | 값 / 구성 | 용도 |
| --- | --- | --- |
| spacingHorizontalNone / spacingVerticalNone | 0 | 레이아웃 방향에 따라 축을 선택합니다. 간격 단계는 공통으로 사용합니다. |
| spacingHorizontalXXS / spacingVerticalXXS | 2px | 레이아웃 방향에 따라 축을 선택합니다. 간격 단계는 공통으로 사용합니다. |
| spacingHorizontalXS / spacingVerticalXS | 4px | 레이아웃 방향에 따라 축을 선택합니다. 간격 단계는 공통으로 사용합니다. |
| spacingHorizontalSNudge / spacingVerticalSNudge | 6px | 레이아웃 방향에 따라 축을 선택합니다. 아이콘 정렬을 미세 조정합니다. |
| spacingHorizontalS / spacingVerticalS | 8px | 레이아웃 방향에 따라 축을 선택합니다. 간격 단계는 공통으로 사용합니다. |
| spacingHorizontalMNudge / spacingVerticalMNudge | 10px | 레이아웃 방향에 따라 축을 선택합니다. 아이콘 정렬을 미세 조정합니다. |
| spacingHorizontalM / spacingVerticalM | 12px | 레이아웃 방향에 따라 축을 선택합니다. 간격 단계는 공통으로 사용합니다. |
| spacingHorizontalL / spacingVerticalL | 16px | 레이아웃 방향에 따라 축을 선택합니다. 간격 단계는 공통으로 사용합니다. |
| spacingHorizontalXL / spacingVerticalXL | 20px | 레이아웃 방향에 따라 축을 선택합니다. 간격 단계는 공통으로 사용합니다. |
| spacingHorizontalXXL / spacingVerticalXXL | 24px | 레이아웃 방향에 따라 축을 선택합니다. 간격 단계는 공통으로 사용합니다. |
| spacingHorizontalXXXL / spacingVerticalXXXL | 32px | 레이아웃 방향에 따라 축을 선택합니다. 간격 단계는 공통으로 사용합니다. |
### React 웹 타이포그래피 조합

| 이름 / 규칙 | 값 / 구성 | 용도 |
| --- | --- | --- |
| Caption2 · fontSizeBase100 | 10px / 14px / 400 | 크기 / 줄 높이 / 굵기. lineHeightBase100과 함께 사용하세요. |
| Caption1 · fontSizeBase200 | 12px / 16px / 400 | 크기 / 줄 높이 / 굵기. lineHeightBase200과 함께 사용하세요. |
| Body1 · fontSizeBase300 | 14px / 20px / 400 | 크기 / 줄 높이 / 굵기. lineHeightBase300과 함께 사용하세요. |
| Body2 / Subtitle2 · fontSizeBase400 | 16px / 22px / 400 / 600 | 크기 / 줄 높이 / 굵기. lineHeightBase400과 함께 사용하세요. |
| Subtitle1 · fontSizeBase500 | 20px / 28px / 600 | 크기 / 줄 높이 / 굵기. lineHeightBase500과 함께 사용하세요. |
| Title3 · fontSizeBase600 | 24px / 32px / 600 | 크기 / 줄 높이 / 굵기. lineHeightBase600과 함께 사용하세요. |
| Title2 · fontSizeHero700 | 28px / 36px / 600 | 크기 / 줄 높이 / 굵기. lineHeightHero700과 함께 사용하세요. |
| Title1 · fontSizeHero800 | 32px / 40px / 600 | 크기 / 줄 높이 / 굵기. lineHeightHero800과 함께 사용하세요. |
| LargeTitle · fontSizeHero900 | 40px / 52px / 600 | 크기 / 줄 높이 / 굵기. lineHeightHero900과 함께 사용하세요. |
| Display · fontSizeHero1000 | 68px / 92px / 600 | 크기 / 줄 높이 / 굵기. lineHeightHero1000과 함께 사용하세요. |
### 글꼴 모음 및 굵기

| 이름 / 규칙 | 값 / 구성 | 용도 |
| --- | --- | --- |
| fontFamilyBase | 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif | 공급자가 기본으로 제공하는 웹 스택입니다. 주변 사이트의 글꼴을 Fluent 글꼴이라고 부르지 마세요. |
| fontFamilyMonospace | Consolas, 'Courier New', Courier, monospace | 코드 및 기술 식별자입니다. |
| fontWeightRegular / Medium / Semibold / Bold | 400 / 500 / 600 / 700 | 타입 스케일에 맞는 역할별 굵기를 사용하세요. |
### 모양 토큰

| 이름 / 규칙 | 값 / 구성 | 용도 |
| --- | --- | --- |
| borderRadiusNone | 0 | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
| borderRadiusSmall | 2px | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
| borderRadiusMedium | 4px | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
| borderRadiusLarge | 6px | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
| borderRadiusXLarge | 8px | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
| borderRadius2XLarge | 12px | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
| borderRadius3XLarge | 16px | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
| borderRadius4XLarge | 24px | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
| borderRadius5XLarge | 32px | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
| borderRadius6XLarge | 40px | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
| borderRadiusCircular | 10000px | 먼저 컴포넌트 기본값을 사용하세요. 이 토큰은 사용할 수 있는 값이지, 모든 표면에 적용해야 하는 규칙은 아닙니다. |
### 모션 지속 시간 단계

| 이름 / 규칙 | 값 / 구성 | 용도 |
| --- | --- | --- |
| durationUltraFast | 50ms | 사용 가능한 런타임 토큰입니다. 이 예시에서는 호버에 Fast를 사용하고, 모션 감소 설정에서는 전환 효과를 제거합니다. |
| durationFaster | 100ms | 사용 가능한 런타임 토큰입니다. 이 예시에서는 호버에 Fast를 사용하고, 모션 감소 설정에서는 전환 효과를 제거합니다. |
| durationFast | 150ms | 사용 가능한 런타임 토큰입니다. 이 예시에서는 호버에 Fast를 사용하고, 모션 감소 설정에서는 전환 효과를 제거합니다. |
| durationNormal | 200ms | 사용 가능한 런타임 토큰입니다. 이 예시에서는 호버에 Fast를 사용하고, 모션 감소 설정에서는 전환 효과를 제거합니다. |
| durationGentle | 250ms | 사용 가능한 런타임 토큰입니다. 이 예시에서는 호버에 Fast를 사용하고, 모션 감소 설정에서는 전환 효과를 제거합니다. |
| durationSlow | 300ms | 사용 가능한 런타임 토큰입니다. 이 예시에서는 호버에 Fast를 사용하고, 모션 감소 설정에서는 전환 효과를 제거합니다. |
| durationSlower | 400ms | 사용 가능한 런타임 토큰입니다. 이 예시에서는 호버에 Fast를 사용하고, 모션 감소 설정에서는 전환 효과를 제거합니다. |
| durationUltraSlow | 500ms | 사용 가능한 런타임 토큰입니다. 이 예시에서는 호버에 Fast를 사용하고, 모션 감소 설정에서는 전환 효과를 제거합니다. |
### 테마 변경은 색상 반전이 아니라 의미에 따른 변경입니다

| 이름 / 규칙 | 값 / 구성 | 용도 |
| --- | --- | --- |
| colorNeutralBackground1 | #292929 | 라이트 테마의 #ffffff 대신 다크 기본 표면을 사용합니다. |
| colorNeutralForeground1 | #ffffff | 라이트 테마의 #242424 대신 다크 기본 텍스트를 사용합니다. |
| colorNeutralForeground2 | #d6d6d6 | 다크 테마의 보조 텍스트입니다. |
| colorBrandBackground | brandWeb[70] = #115ea3 | 다크 테마의 기본 채우기 색상입니다. 라이트 모드에서는 brandWeb[80]을 사용합니다. |
| colorBrandForegroundLink | brandWeb[100] = #479ef5 | 다크 테마의 링크 역할입니다. 기본 버튼의 채우기 색상과는 다릅니다. |
## Microsoft Fluent 2 디자인 시스템 완벽 가이드

실제 인터페이스를 위해 이 시스템을 선택하고 적용하고 검토하는 실용 가이드.

완전한 안내와 구현 시작점, 소스 링크.

## Microsoft Fluent 2 디자인 시스템과 React v9 구현

### 웹 구현을 명시적으로 선택하세요

> Fluent 2는 크로스 플랫폼 디자인 언어입니다. 이 가이드에서는 더 구체적이고 바로 사용할 수 있는 선택지인 통합형 React v9 라이브러리 @fluentui/react-components를 사용합니다. 루트에는 FluentProvider를 두고, 이 예시에서는 webLightTheme 또는 webDarkTheme를 사용합니다. Teams 테마는 별도의 브랜드 설정입니다. Teams에서 익숙한 보라색은 기본 파란색 웹 색상 단계가 아닙니다. Windows, iOS, Android의 네이티브 리소스도 서로 바꿔 쓸 수 있는 React 패키지가 아닙니다.

아래 전체 예시는 프로젝트 개요와 로컬 액세스 목록을 렌더링합니다. Title3와 Body1로 계층을 만들고, TabList와 Tab으로 두 가지 보기를 제공하며, Dialog와 Field, Input, Select를 조합해 액세스 여부를 결정합니다. 이 코드는 실제 사용자 초대를 수행한다고 주장하지 않습니다. 이처럼 구분하면 공유 서비스를 지어내지 않고도 컴포넌트 동작을 명확하게 보여줄 수 있습니다.

지원 사양 읽기

### Provider가 실제로 제공하는 기능

FluentProvider는 하위 트리에 테마 변수를 적용합니다. tokens.colorNeutralBackground2로 스타일을 지정한 사용자 지정 표면은 선택한 테마를 따르지만, 흰색으로 하드코딩한 표면은 그렇지 않습니다. 테마가 필요한 컴포넌트의 상위에 Provider를 배치하세요. 라이브러리는 스타일링 런타임을 통해 컴포넌트 스타일을 적용하므로, 예시에서 존재하지 않는 fluent.css 파일을 가져오지 않습니다.

지원 사양 읽기

## Microsoft Fluent 2 디자인 시스템의 의미 기반 색상과 실제 웹 브랜드 색상 단계

### 선택한 브랜드에 맞춰 별칭 값을 확인하세요

라이트 별칭 소스는 colorBrandBackground를 brand[80]에 연결합니다. 웹 테마는 brandWeb를 제공하며, 여기서 80단계 값은 #0f6cbd입니다. 따라서 이 예시에서 확인되는 기본 채우기 색상은 #0f6cbd입니다. 별칭 파일에는 이전 버전의 예시용 16진수 주석이 남아 있습니다. 이 주석을 현재 웹 팔레트의 값으로 읽으면 잘못된 결과가 나옵니다. 값은 주석이 아니라 소스 간 연결 관계로 확인해야 합니다.

개요에는 colorNeutralForeground1을, 보조 레이블에는 colorNeutralForeground2를, 주변 작업 공간에는 colorNeutralBackground3를 사용하세요. 기본 작업에는 colorBrandBackground와 colorNeutralForegroundOnBrand를 사용할 수 있습니다. 테마가 바뀌어도 역할 이름은 일관된 기준으로 유지됩니다. 사람의 권한은 텍스트로 표시합니다. 파란색 테두리나 탭 표시선은 선택 상태를 나타낼 뿐, 권한을 뜻하지 않습니다.

지원 사양 읽기

### 다크 모드에서는 매핑이 달라집니다

다크 모드에서는 기본 표면이 #292929로, 기본 텍스트가 #ffffff로 설정됩니다. 기본 브랜드 채우기에는 70단계를, 링크 전경색에는 100단계를 사용합니다. 따라서 미리보기에서는 페이지 배경만 뒤집는 대신 토큰 역할을 함께 변경합니다. 전환 후 액세스 대화 상자, 선택된 탭, 약한 대비의 메타데이터를 비교해 보세요. 모두 같은 테마 경계에 속합니다.

지원 사양 읽기

## Microsoft Fluent 2 디자인 시스템 타이포그래피: 텍스트 역할을 런타임 토큰에 매핑하기

### 크기와 줄 높이의 짝을 사용하세요

Body1은 기준 본문 스타일로, 크기는 14px이고 줄 높이는 20px입니다. Caption1은 12/16, Subtitle2는 세미볼드 굵기의 16/22, Title3는 세미볼드 굵기의 24/32입니다. 이 조합을 사용하면 임의의 중간 크기를 만들지 않고도 제목, 레이블, 본문 사이에 뚜렷한 계층을 만들 수 있습니다. 위 표에는 참고용으로 더 큰 단계도 포함되어 있지만, 간결한 작업 공간에 디스플레이 크기의 헤드라인이 꼭 필요한 것은 아닙니다.

React 토큰 소스와 디자인 웹사이트는 유용한 세부 사항 하나에서 현재 차이가 있습니다. 런타임 소스의 fontSizeBase500 / lineHeightBase500은 20/28인 반면, 웹 디자인 표의 Subtitle1은 20/26입니다. 이 예시에서는 런타임의 값을 따릅니다. 이런 차이는 목업에 맞는 숫자를 임의로 조합하지 말고, 버전과 구현을 확인해야 한다는 신호로 보세요.

지원 사양 읽기

### 텍스트 프리셋 및 글꼴 스택

시각적 프리셋과 함께 의미에 맞는 HTML를 사용하세요. Title3 as="h1"은 제목입니다. 스타일만 지정한다고 문서 개요가 만들어지는 것은 아닙니다. 기본 글꼴 스택은 Segoe UI로 시작하며 플랫폼별 대체 글꼴을 포함합니다. 스크린샷과 똑같이 보이게 하려고 독점 글꼴을 번들로 포함하지 마세요. 예제는 문서에 명시된 글꼴 스택을 사용하므로, 실제 글리프는 보는 기기에 설치된 글꼴에 따라 달라집니다.

사용자 지정 텍스트에는 글꼴 모음, 크기, 굵기, 줄 높이를 묶어 제공하는 typographyStyles.body1을 사용하세요. fontSizeBase300만 복사하는 것보다 이 조합을 사용하는 편이 좋습니다. 권한 목록에는 줄바꿈이 적용되는 읽기 쉬운 이메일 주소를 사용합니다. 권한 레이블은 세미볼드로 강조하되 페이지 제목보다 두드러지지 않게 합니다.

지원 사양 읽기

## Microsoft Fluent 2 디자인 시스템: 작업 공간의 간격, 형태 및 밀도

### React의 간격 이름은 디자인 도구의 이름과 다릅니다

Fluent의 일반 레이아웃 가이드에서는 전역 크기 이름과 4단위 리듬을 사용하며, 정렬 조정에는 2, 6, 10도 포함됩니다. React에는 축별 별칭이 있습니다. spacingHorizontalS는 8px, M은 12px, L은 16px, XXL은 24px이며, 이에 대응하는 세로 이름도 있습니다. size120을 spacingHorizontal120으로 바꾸지 마세요. React에는 해당 식별자가 없습니다.

이 구성에서는 패널 안쪽 여백으로 24px, 양식 필드 사이 간격으로 16px, 권한 행 사이 간격으로 12px를 사용합니다. 정확한 페이지 너비와 2열 배치는 애플리케이션에서 결정할 사항이지, 공식 Fluent 중단점이 아닙니다. 화면이 좁으면 보조 팀 정보가 브리프 아래로 이동하지만, 탭 레이블 두 개는 계속 표시됩니다.

지원 사양 읽기

### 형태는 구성 요소의 크기를 나타냅니다

사용할 수 있는 반경 토큰에는 4px인 Medium과 8px인 XLarge가 있습니다. 하지만 사용할 수 있다고 해서 모든 컨트롤에 같은 반경을 적용해야 하는 것은 아닙니다. HTML 예제에서는 명시적인 검토 선택으로 컨트롤에 4px, 문서 표면에 8px 형태를 사용합니다. 선택한 디자인 요구사항에 따라 재정의할 이유가 없다면 기본 구성 요소는 지원되는 고유한 구조를 유지해야 합니다. 다른 형태를 강제로 적용하려고 생성된 클래스 이름에 직접 접근하지 마세요.

지원 사양 읽기

## Microsoft Fluent 2 디자인 시스템의 TabList: 선택, 패널 및 키보드 동작

### 선택된 값은 애플리케이션 상태입니다

이 예제는 React 상태에서 탭을 관리하고 이를 selectedValue에 전달합니다. onTabSelect는 data.value를 받아 상태를 업데이트합니다. 각 Tab에는 고정된 value가 있으며 aria-controls를 통해 해당 패널을 참조합니다. 각 패널에는 role="tabpanel", aria-labelledby 및 숨김 여부를 결정하는 조건이 있습니다. 구성 요소는 프로젝트 브리프나 권한 목록을 제공하지 않으므로, 해당 내용은 명시적으로 렌더링해야 합니다.

API에서는 포커스 이동과 선택을 분리합니다. selectTabOnFocus의 기본값은 false입니다. 독립 실행형 예제에서 왼쪽/오른쪽 화살표와 Home/End 키로 포커스를 이동하고, Enter 또는 Space 키로 포커스된 탭을 활성화합니다. 이 수동 활성화 방식은 예상치 못한 콘텐츠 전환 없이 탭 항목을 살펴볼 수 있게 합니다. 탭 레이블은 짧게 유지하세요. 공식 레이아웃 가이드에 따르면 가로 탭은 자동으로 줄바꿈되거나 스크롤되지 않습니다.

지원 사양 읽기

### 범주가 늘어나면 다른 패턴을 선택하세요

Brief와 People은 한 작업 공간에서 밀접하게 관련된 두 가지 보기이므로 탭이 적합합니다. 조직의 모든 프로젝트를 나열하는 것은 탐색이지, 탭을 한 줄 더 추가하는 것이 아닙니다. 화면 너비가 좁아져 범주가 들어가지 않는다면 의도적으로 오버플로 또는 다른 선택 패턴을 설계하세요. 읽을 수 없을 정도로 레이블을 줄이는 것은 반응형 전략이 아닙니다.

지원 사양 읽기

## Microsoft Fluent 2 디자인 시스템의 Field: 접근성 있는 레이블과 의미 있는 유효성 검사

### Field는 스타일을 입힌 입력란이 아니라 여러 요소의 조합입니다

Field는 레이블 슬롯, 하위 컨트롤, 선택적 도움말 및 유효성 검사 메시지를 결합합니다. 문서에 명시된 컨트롤 계약에는 id, aria-labelledby, aria-describedby, aria-invalid 및 aria-required가 포함됩니다. Fluent 입력란은 직접 자식으로 사용할 수 있으며, 사용자 지정 컨트롤은 렌더 함수의 props를 사용할 수 있습니다. 따라서 이메일 Input을 Field로 감싸면 입력란 위에 관련 없는 텍스트를 배치하는 것보다 유용합니다.

이 예제에서는 유효하지 않은 값으로 제출을 시도한 뒤에만 validationState가 error가 됩니다. 애플리케이션은 이메일 형식에 맞는 값인지와 메모리 내 목록에 중복 항목이 있는지를 확인합니다. 입력한 값을 유지하면서 validationMessage로 수정 방법을 안내합니다. Field는 해당 결과를 표시하고 연결하지만, 주소가 실제로 존재하는지 확인하거나 초대를 전송하거나 접근 권한을 부여하지는 않습니다.

지원 사양 읽기

### 유효성 검사 상태와 권한 상태를 구분하세요

권한 Select에는 Can view와 Can edit가 있으며, 이메일 필드에는 별도의 오류 상태가 있습니다. 주소 하나가 불완전하다고 대화상자 전체를 빨갛게 표시하지 마세요. required는 접근성을 위한 표시이며, 서버의 권한 부여 또는 유효성 검사를 대신하지 않습니다. 예제 양식은 명시적인 유효성 검사 메시지를 일관되게 보여 주기 위해 noValidate를 사용합니다. 프로덕션 서비스에서는 요청을 다시 검증해야 합니다.

- 주소가 비어 있거나 형식에 맞지 않으면 대화상자를 열어 둔 채 올바른 형식을 안내합니다.

- 주소가 로컬 권한 목록에 이미 있으면 중복임을 알리는 메시지를 표시하고 항목을 추가하지 않습니다.

- 새 주소가 유효하면 선택한 권한을 로컬에 추가하고 대화상자를 닫은 뒤, 로컬에서만 처리되었음을 정확히 알립니다.

지원 사양 읽기

## Microsoft Fluent 2 디자인 시스템의 Dialog: 지원되는 구조에 포함된 양식

### 제출 버튼은 양식 안에 두세요

구성은 Dialog, DialogTrigger, DialogSurface, form 요소, 그리고 DialogTitle, DialogContent, DialogActions를 포함하는 DialogBody로 이루어집니다. 제출 버튼이 있는 바닥글도 제출에 참여하도록 form이 본문을 감쌉니다. 취소 버튼은 type="button"으로 지정하고 닫는 DialogTrigger 안에 둡니다. 그렇지 않으면 기본 버튼 유형 때문에 실수로 양식이 제출될 수 있습니다.

open과 onOpenChange가 표시 여부를 제어합니다. disableButtonEnhancement는 버튼 동작을 이미 제공하는 Fluent Button을 트리거가 중복으로 보강하지 않도록 합니다. 성공 처리기는 로컬 유효성 검사를 통과한 뒤에만 대화상자를 닫습니다. 유효성 검사에 실패하면 같은 이메일과 권한을 유지해 수정할 수 있게 합니다.

지원 사양 읽기

### 실제 결정에 맞는 모달 동작을 선택하세요

이 짧은 액세스 결정에는 기본 모달 유형을 사용합니다. 비모달 대화상자라면 백그라운드 작업을 계속할 수 있고, 경고 대화상자는 긴급한 결정에 사용하며 닫기 동작에 더 엄격한 제약이 있습니다. 일반적인 액세스 양식에는 경고 대화상자의 의미가 필요하지 않습니다. 공식 가이드에서는 대화상자 안에 포커스를 두고 닫은 뒤에는 트리거로 포커스를 돌려보내도록 안내합니다. 독립형 해석에서는 기본 dialog 요소를 사용하고 트리거의 포커스를 명시적으로 복원합니다.

지원 사양 읽기

## Microsoft Fluent 2 디자인 시스템 테마를 깨뜨리지 않고 사용자 지정 스타일 적용하기

### makeStyles와 시맨틱 변수 사용하기

전체 코드에서는 표면, 간격, 모서리 반경을 위한 토큰과 함께 makeStyles를 사용합니다. 덕분에 사용자 지정 패널도 provider의 스타일과 조화를 이룹니다. 생성된 클래스 이름은 구현 세부 사항이므로 DevTools에서 복사해 스타일시트에 붙여 넣지 마세요. 테마를 실험할 때는 provider 테마를 변경하고 기본 컨트롤과 사용자 지정 영역을 함께 살펴보세요.

게시된 duration 램프는 50ms부터 500ms까지입니다. HTML 예제에서는 단순한 호버 변경에 durationFast(150ms)를 사용하고 prefers-reduced-motion 설정에서는 해당 전환을 제거합니다. 이는 사용 가능한 토큰을 활용한 애플리케이션의 선택일 뿐, 모든 Fluent 상호작용의 지속 시간이 150ms여야 한다는 뜻은 아닙니다. 모션을 제거해도 콘텐츠와 오류 메시지를 이해할 수 있습니다.

지원 사양 읽기

### 해석의 적용 범위를 확인하세요

포함된 예제에는 기본 Fluent 구현이 아니라 HTML/CSS 해석이라고 명시되어 있습니다. 미리보기에서 프레임워크를 다운로드하지 않고도 검증된 값과 액세스 대화상자 상호작용을 보여 줍니다. 기본 스타일, 포털, 포커스 관리, 컴포넌트 업데이트를 평가하려면 전체 React 예제를 사용하세요. 독립형 상호작용 검사를 통과했다는 것은 이 예제에 대한 근거일 뿐, 실제 공유 애플리케이션의 인증을 의미하지는 않습니다.

지원 사양 읽기

## Microsoft Fluent 2 디자인 시스템 전체 구성 설치 및 검증하기

### 작업 개요가 아닌 실행 가능한 진입점

이 설정은 id가 root인 요소가 있는 기존 React 애플리케이션을 대상으로 합니다. v9 제품군을 설치한 다음 JSX 진입점을 애플리케이션에 추가하세요. import, provider, 컴포넌트 트리, 핸들러, 상태, root 렌더링을 한데 제공합니다. 외부 공유 API는 호출하지 않습니다. 시작 프로젝트에서 이미 App을 렌더링한다면 root 진입점은 그대로 두고, root를 새로 만들지 말고 App을 내보내세요.

해결된 패키지 버전을 lockfile에 기록하세요. 연결된 소스 참조는 살펴본 React v9 구현을 설명하지만, master URL은 변경될 수 있습니다. 업그레이드하기 전에 내보낸 테마와 여기서 사용하는 Field, TabList, Dialog의 정확한 동작을 비교하세요. 이 설정에서는 v8 @fluentui/react 컴포넌트와 v9 예제를 함께 사용하지 않습니다.

1. 기존 React 프로젝트에 @fluentui/react-components@9를 설치하고 생성된 lockfile을 유지하세요.

2. 제공된 진입점을 하나의 FluentProvider 안에서 렌더링하고, 초기 테마로 webLightTheme을 사용하세요.

3. 대화상자를 열고 잘못된 주소를 제출한 뒤 수정하세요. 추가한 사람이 로컬 상태에만 표시되는지 확인하세요.

4. webDarkTheme과 키보드 탐색에서도 반복하여 패널 연결, 오류 알림, 포커스 복원을 확인하세요.

지원 사양 읽기

### 실제 공유 제품에 연결할 항목

서비스 계약을 정의한 뒤에만 로컬 추가 동작을 대체하세요. 계약에는 인증된 작업자, 대상 작업 공간, 허용되는 권한 값, 초대 결과, 중복 처리 방식이 포함되어야 합니다. 서비스가 요청을 거부하면 대화상자를 열린 상태로 유지하고, 액세스가 허용됐다고 성급하게 표시하지 말고 확인된 결과를 보여 주세요. 이런 규칙은 Fluent의 범위 밖이지만, 이 코드는 일반적인 제출 버튼 뒤에 숨기지 않고 연동 지점을 명확히 보여 줍니다.

지원 사양 읽기

## Microsoft Fluent 2 디자인 시스템 컴포넌트 예제

### 완전한 독립형 인터랙티브 예제

문서 전체를 index.html로 저장하고 브라우저에서 여세요. Fluent UI React v9 웹 토큰과 문서화된 Field, TabList, Dialog 구조를 독립형 HTML/CSS 방식으로 해석한 예제입니다. 기본 Fluent 컴포넌트 런타임은 아닙니다.

```
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Fluent web workspace study</title><style>
:root{color-scheme:light;--colorNeutralForeground1:#242424;--colorNeutralForeground2:#424242;--colorNeutralForeground3:#616161;--colorNeutralBackground1:#fff;--colorNeutralBackground2:#fafafa;--colorNeutralBackground3:#f5f5f5;--colorNeutralStroke1:#d1d1d1;--colorNeutralStroke2:#e0e0e0;--colorBrandBackground:#0f6cbd;--colorBrandBackgroundHover:#115ea3;--colorBrandBackgroundPressed:#0c3b5e;--colorBrandBackground2:#ebf3fc;--colorBrandForegroundLink:#115ea3;--colorNeutralForegroundOnBrand:#fff;--borderRadiusMedium:4px;--borderRadiusXLarge:8px;--spacingHorizontalS:8px;--spacingHorizontalM:12px;--spacingHorizontalL:16px;--spacingHorizontalXXL:24px;--durationFast:150ms}
[data-theme=dark]{color-scheme:dark;--colorNeutralForeground1:#fff;--colorNeutralForeground2:#d6d6d6;--colorNeutralForeground3:#adadad;--colorNeutralBackground1:#292929;--colorNeutralBackground2:#1f1f1f;--colorNeutralBackground3:#141414;--colorNeutralStroke1:#666;--colorNeutralStroke2:#525252;--colorBrandBackground:#115ea3;--colorBrandBackgroundHover:#0f6cbd;--colorBrandBackground2:#082338;--colorBrandForegroundLink:#479ef5}
*{box-sizing:border-box}body{margin:0;background:var(--colorNeutralBackground3);color:var(--colorNeutralForeground1);font-family:'Segoe UI','Segoe UI Web (West European)',-apple-system,BlinkMacSystemFont,Roboto,'Helvetica Neue',sans-serif;font-size:14px;line-height:20px}button,input,select{font:inherit}button{cursor:pointer;min-height:32px;padding:5px 12px;border:1px solid var(--colorNeutralStroke1);border-radius:var(--borderRadiusMedium);background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);font-weight:600;transition:background var(--durationFast)}button:hover{background:var(--colorNeutralBackground3)}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--colorBrandForegroundLink);outline-offset:3px}.primary{background:var(--colorBrandBackground);color:var(--colorNeutralForegroundOnBrand);border-color:transparent}.primary:hover{background:var(--colorBrandBackgroundHover)}.primary:active{background:var(--colorBrandBackgroundPressed)}.topbar{display:flex;justify-content:space-between;align-items:center;padding:12px 24px;background:var(--colorNeutralBackground1);border-bottom:1px solid var(--colorNeutralStroke2);gap:16px}.brand{display:flex;align-items:center;gap:12px;font-weight:600}.mark{display:grid;grid-template-columns:repeat(2,7px);gap:3px}.mark i{width:7px;height:7px;background:var(--colorBrandBackground)}.top-note{font-size:12px;color:var(--colorNeutralForeground3)}.page{max-width:1100px;margin:auto;padding:24px}.heading{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:16px}.eyebrow{font-size:12px;line-height:16px;color:var(--colorNeutralForeground3);margin-bottom:4px}h1{font-size:24px;line-height:32px;font-weight:600;margin:0 0 4px}h2{font-size:20px;line-height:28px;font-weight:600;margin:0 0 16px}h3{font-size:16px;line-height:22px;font-weight:600;margin:20px 0 8px}p{margin:0 0 12px}.muted{color:var(--colorNeutralForeground3)}.tabs{display:flex;gap:16px;border-bottom:1px solid var(--colorNeutralStroke2);margin-bottom:24px}.tab{position:relative;background:none;border:0;border-radius:0;font-weight:400;padding:12px 4px;color:var(--colorNeutralForeground2)}.tab[aria-selected=true]{font-weight:600;color:var(--colorBrandForegroundLink)}.tab[aria-selected=true]:after{content:'';position:absolute;bottom:0;left:4px;right:4px;height:3px;background:var(--colorBrandBackground);border-radius:3px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 240px;gap:24px}.document,.aside,.people{background:var(--colorNeutralBackground1);border:1px solid var(--colorNeutralStroke2);border-radius:var(--borderRadiusXLarge);padding:24px}.document{max-width:720px}.docmeta{display:flex;gap:8px;align-items:center;color:var(--colorNeutralForeground3);font-size:12px;line-height:16px;margin-bottom:24px}.avatar{flex:none;display:grid;place-items:center;background:var(--colorBrandBackground2);color:var(--colorBrandForegroundLink);font-weight:600;width:32px;height:32px;border-radius:50%}.quote{padding:12px 16px;background:var(--colorNeutralBackground2);border-left:3px solid var(--colorBrandBackground);margin:16px 0}.checklist{list-style:none;padding:0;margin:12px 0}.checklist li{display:flex;gap:8px;margin:8px 0}.checklist input{accent-color:var(--colorBrandBackground);width:16px;height:16px;margin-top:2px}.aside{padding:20px;height:fit-content}.aside h2{font-size:16px;line-height:22px;margin-bottom:16px}.person{display:flex;gap:8px;align-items:center;margin-bottom:16px}.person p{margin:0}.small{font-size:12px;line-height:16px}.aside hr{border:0;border-top:1px solid var(--colorNeutralStroke2);margin:20px 0}.tag{display:inline-block;border:1px solid var(--colorNeutralStroke1);padding:2px 8px;border-radius:4px;font-size:12px;color:var(--colorNeutralForeground2)}.people-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;gap:12px}.people-head h2{margin:0}.people ul{list-style:none;padding:0;margin:0}.member{display:flex;align-items:center;gap:12px;padding:16px 0;border-top:1px solid var(--colorNeutralStroke2)}.member-info{flex:1;min-width:0;overflow-wrap:anywhere}.member-info p{margin:0}.member .tag{flex:none}.caption{margin:20px 0 0;color:var(--colorNeutralForeground3);font-size:12px;line-height:16px}.status{margin-top:16px;overflow-wrap:anywhere;color:var(--colorNeutralForeground2)}.status:empty{display:none}[hidden]{display:none!important}dialog{width:min(520px,calc(100vw - 32px));padding:24px;border:1px solid var(--colorNeutralStroke1);border-radius:8px;background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);max-height:calc(100dvh - 32px);overflow:auto}dialog::backdrop{background:rgba(0,0,0,.4)}dialog h2{font-size:20px;line-height:28px;margin-bottom:12px}.field{margin:20px 0}.field label{display:block;font-weight:600;margin-bottom:8px}.required{color:#b10e1e}.field input,.field select{width:100%;min-height:36px;background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);padding:7px 12px;border:1px solid var(--colorNeutralStroke1);border-bottom-color:var(--colorNeutralForeground3);border-radius:4px}.field input:focus{border-bottom:2px solid var(--colorBrandBackground);padding-bottom:6px}.hint{font-size:12px;line-height:16px;color:var(--colorNeutralForeground3);margin-top:8px}.error{font-size:12px;line-height:16px;color:#b10e1e;margin-top:8px}.field input[aria-invalid=true]{border-color:#b10e1e}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:24px}[data-theme=dark] .error,[data-theme=dark] .required{color:#ff99a4}footer{padding:0 24px 20px;text-align:center;font-size:12px;color:var(--colorNeutralForeground3)}@media(max-width:700px){.page{padding:16px}.topbar{padding:12px 16px}.top-note{display:none}.layout{grid-template-columns:1fr}.document,.people{padding:20px}.aside{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px}.aside h2,.aside hr{grid-column:1/-1}.aside hr{margin:0}.heading{align-items:flex-start}.heading .primary{flex:none}.member{flex-wrap:wrap}.member .tag{margin-left:44px}.topbar button,.heading button{min-height:40px}}@media(max-width:420px){h1{font-size:20px;line-height:28px}.heading{flex-direction:column;gap:12px}.aside{display:block}.member .tag{margin-left:0}}@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style></head><body>
<header class="topbar"><div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i><i></i><i></i></span>Northstar workspace <span class="top-note">/ Product team</span></div><button id="theme" aria-pressed="false">Dark theme</button></header>
<main class="page"><div class="heading"><div><div class="eyebrow">Team workspace · October launch</div><h1>Launch workspace</h1><p class="muted">One brief. A clear decision. The right people.</p></div><button class="primary" id="share">Add a person</button></div>
<div class="tabs" role="tablist" aria-label="Workspace views"><button class="tab" id="brief-tab" role="tab" aria-controls="brief-panel" aria-selected="true" tabindex="0">Brief</button><button class="tab" id="people-tab" role="tab" aria-controls="people-panel" aria-selected="false" tabindex="-1">People <span id="count">2</span></button></div>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab" tabindex="0"><div class="layout"><article class="document"><div class="docmeta"><span class="avatar" aria-hidden="true">AC</span><span>Alex Chen · Product design<br>Updated today at 09:30</span></div><h2>A more welcoming first project</h2><p>Help a new team move from an empty workspace to its first useful result. This review focuses on the welcome screen and the first-project setup.</p><h3>The decision</h3><div class="quote">Should a sample project be created automatically, or offered as an option?</div><p>We want a clear starting point without filling the workspace with content someone did not choose. Review both paths before the team walkthrough.</p><h3>Review checklist</h3><ul class="checklist"><li><input id="c1" type="checkbox" checked><label for="c1">Welcome copy explains the next step</label></li><li><input id="c2" type="checkbox"><label for="c2">Empty workspace offers a sample project</label></li><li><input id="c3" type="checkbox"><label for="c3">Keyboard path reaches the first project action</label></li></ul><p class="small muted">Checklist changes are local to this example.</p></article><aside class="aside"><h2>Working together</h2><div class="person"><span class="avatar" aria-hidden="true">AC</span><div><p>Alex Chen</p><span class="small muted">Workspace owner</span></div></div><div class="person"><span class="avatar" aria-hidden="true">MP</span><div><p>Maya Patel</p><span class="small muted">Can edit</span></div></div><hr><div><h3>Next review</h3><p>Thursday, 10:00<br><span class="muted small">Team walkthrough · UTC</span></p></div><div><h3>Access</h3><span class="tag">Specific people</span><p class="small muted" style="margin-top:8px">Adding a person here updates only this local preview.</p></div></aside></div></section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab" tabindex="0" hidden><div class="people"><div class="people-head"><h2>People with access</h2><span class="tag">Local example</span></div><p class="muted">The roles below are fictional. No invitation or real access change is made.</p><ul id="members"><li class="member"><span class="avatar" aria-hidden="true">AC</span><div class="member-info"><p>Alex Chen</p><span class="small muted">alex@example.com</span></div><span class="tag">Owner</span></li><li class="member"><span class="avatar" aria-hidden="true">MP</span><div class="member-info"><p>Maya Patel</p><span class="small muted">maya@example.com</span></div><span class="tag">Can edit</span></li></ul></div></section><p class="status" id="status" role="status"></p><p class="caption">Fluent UI React v9 web-token study · HTML/CSS interpretation, not native library components. Try tabs, theme, checkboxes and the access form.</p></main>
<dialog id="access" aria-labelledby="dialog-title"><form id="access-form" novalidate><h2 id="dialog-title">Add a person to Launch workspace</h2><p class="muted">Choose an address and permission. This demonstration updates the local list only.</p><div class="field"><label for="email">Email address <span class="required" aria-hidden="true">*</span></label><input id="email" type="email" autocomplete="email" aria-required="true" aria-describedby="email-hint" autofocus><div class="hint" id="email-hint">For example, jordan@example.com</div><div class="error" id="error" role="alert" hidden></div></div><div class="field"><label for="permission">Permission</label><select id="permission" aria-describedby="permission-hint"><option>Can view</option><option>Can edit</option></select><div class="hint" id="permission-hint">Viewers can read; editors can change the brief in a real application</div></div><div class="actions"><button type="button" id="cancel">Cancel</button><button class="primary" type="submit">Add locally</button></div></form></dialog>
<script>
const $=id=>document.getElementById(id),tabs=[...document.querySelectorAll('[role=tab]')],emails=new Set(['alex@example.com','maya@example.com']);
function selectTab(tab){tabs.forEach(t=>{const chosen=t===tab;t.setAttribute('aria-selected',String(chosen));t.tabIndex=chosen?0:-1;$(t.getAttribute('aria-controls')).hidden=!chosen;});}
tabs.forEach((tab,index)=>{tab.addEventListener('click',()=>selectTab(tab));tab.addEventListener('keydown',event=>{let next;if(event.key==='ArrowRight')next=(index+1)%tabs.length;if(event.key==='ArrowLeft')next=(index+tabs.length-1)%tabs.length;if(event.key==='Home')next=0;if(event.key==='End')next=tabs.length-1;if(next!==undefined){event.preventDefault();tabs.forEach(t=>t.tabIndex=-1);tabs[next].tabIndex=0;tabs[next].focus();}});});
$('theme').onclick=()=>{const dark=document.documentElement.dataset.theme!=='dark';document.documentElement.dataset.theme=dark?'dark':'light';$('theme').textContent=dark?'Light theme':'Dark theme';$('theme').setAttribute('aria-pressed',String(dark));};
const dialog=$('access'),email=$('email'),error=$('error');function clearError(){error.hidden=true;error.textContent='';email.removeAttribute('aria-invalid');email.setAttribute('aria-describedby','email-hint');}function showError(message){error.textContent=message;error.hidden=false;email.setAttribute('aria-invalid','true');email.setAttribute('aria-describedby','email-hint error');email.focus();}
$('share').onclick=()=>{clearError();dialog.showModal();email.focus();};$('cancel').onclick=()=>dialog.close();dialog.addEventListener('close',()=>$('share').focus());dialog.addEventListener('click',event=>{if(event.target===dialog){const r=dialog.getBoundingClientRect();if(event.clientX<r.left||event.clientX>r.right||event.clientY<r.top||event.clientY>r.bottom)dialog.close();}});email.addEventListener('input',clearError);
$('access-form').addEventListener('submit',event=>{event.preventDefault();const value=email.value.trim().toLowerCase();if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)){showError('Enter an email address such as name@example.com');return;}if(emails.has(value)){showError('This address already appears in the local access list');return;}const permission=$('permission').value;emails.add(value);const item=document.createElement('li');item.className='member';const avatar=document.createElement('span');avatar.className='avatar';avatar.setAttribute('aria-hidden','true');avatar.textContent=value.slice(0,2).toUpperCase();const info=document.createElement('div');info.className='member-info';const name=document.createElement('p');name.textContent=value;const sub=document.createElement('span');sub.className='small muted';sub.textContent='Added in this local example';info.append(name,sub);const role=document.createElement('span');role.className='tag';role.textContent=permission;item.append(avatar,info,role);$('members').append(item);$('count').textContent=emails.size;$('status').textContent='Added '+value+' as '+permission.toLowerCase()+' locally. No invitation was sent.';email.value='';clearError();selectTab($('people-tab'));dialog.close();});
</script></body></html>
```

코드 참조

- 이 문서의 액세스 대화상자와 탭은 기본 HTML 컨트롤을 사용합니다. FluentProvider, Field, Dialog 동작을 평가하려면 별도의 React v9 진입점을 실행하세요.

### 전체 네이티브 구현 엔트리

셋업의 전체 구현을 집중적인 API 설명과 함께 여기에 모았습니다. 셋업에 문서화된 패키지 및 빌드 전제 조건을 따르세요.

```
// Terminal: npm install @fluentui/react-components@9
// Existing React application; index.html must contain <div id="root"></div>.
// Entry file: src/main.jsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import {
  FluentProvider, webLightTheme, webDarkTheme, makeStyles, tokens,
  Button, Body1, Title3, TabList, Tab, Field, Input, Select,
  Dialog, DialogTrigger, DialogSurface, DialogBody, DialogTitle,
  DialogContent, DialogActions
} from '@fluentui/react-components';

const useStyles = makeStyles({
  page: { maxWidth: '880px', margin: '0 auto', padding: tokens.spacingHorizontalXXL },
  header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: tokens.spacingHorizontalL },
  panel: { padding: tokens.spacingHorizontalXXL, marginTop: tokens.spacingVerticalL,
    backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusXLarge },
  form: { display: 'grid', gap: tokens.spacingVerticalL },
  row: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: tokens.spacingHorizontalM,
    paddingTop: tokens.spacingVerticalM, paddingBottom: tokens.spacingVerticalM },
});

function Workspace({ dark, setDark }) {
  const styles = useStyles();
  const [tab, setTab] = React.useState('brief');
  const [open, setOpen] = React.useState(false);
  const [email, setEmail] = React.useState('');
  const [permission, setPermission] = React.useState('Can view');
  const [error, setError] = React.useState('');
  const [notice, setNotice] = React.useState('');
  const [people, setPeople] = React.useState([{ email: 'alex@example.com', permission: 'Owner' }]);
  function submit(event) {
    event.preventDefault();
    const value = email.trim().toLowerCase();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      setError('Enter an email address such as name@example.com'); return;
    }
    if (people.some(person => person.email === value)) {
      setError('This address already appears in the local access list'); return;
    }
    setPeople(previous => [...previous, { email: value, permission }]);
    setNotice(`Added ${value} as ${permission.toLowerCase()} in this local example. No invitation was sent.`);
    setEmail(''); setError(''); setOpen(false); setTab('people');
  }
  return <main className={styles.page}>
    <div className={styles.header}>
      <Title3 as="h1">Launch workspace</Title3>
      <Button onClick={() => setDark(!dark)}>{dark ? 'Light theme' : 'Dark theme'}</Button>
    </div>
    <Body1>Project brief and access settings · local demonstration</Body1>
    <TabList aria-label="Workspace views" selectedValue={tab}
      onTabSelect={(_, data) => setTab(data.value)}>
      <Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
      <Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
    </TabList>
    <section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
      hidden={tab !== 'brief'} className={styles.panel} tabIndex={0}>
      <h2>Launch a clearer onboarding flow</h2>
      <p>Review the welcome screen and first-project setup before the team walkthrough.</p>
      <p>Decision needed: should the sample project be created automatically or offered as an option?</p>
    </section>
    <section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
      hidden={tab !== 'people'} className={styles.panel} tabIndex={0}>
      <h2>People with access</h2>
      <ul>{people.map(person => <li key={person.email} className={styles.row}>
        <span>{person.email}</span><strong>{person.permission}</strong>
      </li>)}</ul>
    </section>
    <Dialog open={open} onOpenChange={(_, data) => setOpen(data.open)}>
      <DialogTrigger disableButtonEnhancement><Button appearance="primary">Add a person</Button></DialogTrigger>
      <DialogSurface>
        <form noValidate onSubmit={submit}>
          <DialogBody>
            <DialogTitle>Add a person to Launch workspace</DialogTitle>
            <DialogContent className={styles.form}>
              <p>This example updates local state only. It does not send email or change real access.</p>
              <Field label="Email address" required validationState={error ? 'error' : 'none'}
                validationMessage={error || undefined}>
                <Input type="email" value={email} onChange={(_, data) => { setEmail(data.value); setError(''); }} />
              </Field>
              <Field label="Permission" hint="Viewers can read; editors can change the brief in a real application">
                <Select value={permission} onChange={(_, data) => setPermission(data.value)}>
                  <option>Can view</option><option>Can edit</option>
                </Select>
              </Field>
            </DialogContent>
            <DialogActions>
              <DialogTrigger disableButtonEnhancement><Button type="button">Cancel</Button></DialogTrigger>
              <Button type="submit" appearance="primary">Add locally</Button>
            </DialogActions>
          </DialogBody>
        </form>
      </DialogSurface>
    </Dialog>
    <p role="status">{notice}</p>
  </main>;
}
function App() {
  const [dark, setDark] = React.useState(false);
  return <FluentProvider theme={dark ? webDarkTheme : webLightTheme}>
    <Workspace dark={dark} setDark={setDark} />
  </FluentProvider>;
}
createRoot(document.getElementById('root')).render(<App />);
```

코드 참조

- 기존 React 앱을 위한 완전한 React v9 컴포넌트 구성입니다. 독립형 미리보기는 별도의 HTML/CSS 해석입니다. 이 코드는 로컬 유효성 검사와 상태 업데이트만 수행하며, 이메일 발송이나 실제 권한 변경은 이루어지지 않습니다. 소스 API는 확인했지만, 대상 프로젝트에서 테스트하기 전까지 기본 패키지의 컴파일 가능성을 보장하지 않습니다.

### Field + Input + Select: 레이블이 있는 단일 액세스 양식

아래 전체 설정에 있는 상태 변수와 import를 사용하는 코드 조각입니다. Field는 레이블, 안내 문구, 유효성 검사 표시를 담당하고, 이메일 규칙과 중복 확인은 애플리케이션이 담당합니다.

```
<Field label="Email address" required
  validationState={error ? 'error' : 'none'}
  validationMessage={error || undefined}>
  <Input type="email" value={email}
    onChange={(_, data) => { setEmail(data.value); setError(''); }} />
</Field>
<Field label="Permission" hint="Choose what this person can do">
  <Select value={permission}
    onChange={(_, data) => setPermission(data.value)}>
    <option>Can view</option><option>Can edit</option>
  </Select>
</Field>
```

코드 참조

- required는 접근성을 고려한 필수 입력 표시를 제공합니다. 애플리케이션의 유효성 검사 규칙까지 구현하는 것은 아닙니다.

- validationMessage가 있으면 기본 유효성 검사 상태가 error가 됩니다. 오류를 표시하지 않아야 할 때는 none을 명시적으로 설정하세요.

- Input의 onChange는 data.value에 현재 문자열을 제공합니다. 이 핸들러를 일반적인 DOM 이벤트만 받는 input 핸들러와 혼동하지 마세요.

### 패널과 명시적으로 연결된 제어형 TabList

TabList는 선택과 탭 동작을 제어합니다. 콘텐츠 패널은 직접 렌더링하고, 각 패널의 id와 접근 가능한 이름을 해당 Tab에 연결하세요.

```
<TabList selectedValue={tab} aria-label="Workspace views"
  onTabSelect={(_, data) => setTab(data.value)}>
  <Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
  <Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
</TabList>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
  hidden={tab !== 'brief'} tabIndex={0}>Project brief</section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
  hidden={tab !== 'people'} tabIndex={0}>People with access</section>
```

코드 참조

- selectedValue는 TabList에 지정하고, 각 Tab에는 서로 다른 value를 지정하세요.

- selectTabOnFocus의 기본값은 false입니다. 이 동작을 의도적으로 변경하지 않는 한 화살표 키로 포커스를 이동하는 것과 탭을 활성화하는 것은 별개입니다.

- Fluent의 가로 탭은 자동으로 줄바꿈되거나 스크롤되지 않습니다. 이 예제에서는 두 개의 짧은 레이블을 사용해 탭이 화면에 계속 보이도록 합니다.

## Microsoft Fluent 2 디자인 시스템 AI 에이전트 설정

### 구현 시작점

기존 React 앱을 위한 완전한 React v9 컴포넌트 구성입니다. 독립형 미리보기는 별도의 HTML/CSS 해석입니다. 이 코드는 로컬 유효성 검사와 상태 업데이트만 수행하며, 이메일 발송이나 실제 권한 변경은 이루어지지 않습니다. 소스 API는 확인했지만, 대상 프로젝트에서 테스트하기 전까지 기본 패키지의 컴파일 가능성을 보장하지 않습니다.

```
// Terminal: npm install @fluentui/react-components@9
// Existing React application; index.html must contain <div id="root"></div>.
// Entry file: src/main.jsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import {
  FluentProvider, webLightTheme, webDarkTheme, makeStyles, tokens,
  Button, Body1, Title3, TabList, Tab, Field, Input, Select,
  Dialog, DialogTrigger, DialogSurface, DialogBody, DialogTitle,
  DialogContent, DialogActions
} from '@fluentui/react-components';

const useStyles = makeStyles({
  page: { maxWidth: '880px', margin: '0 auto', padding: tokens.spacingHorizontalXXL },
  header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: tokens.spacingHorizontalL },
  panel: { padding: tokens.spacingHorizontalXXL, marginTop: tokens.spacingVerticalL,
    backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusXLarge },
  form: { display: 'grid', gap: tokens.spacingVerticalL },
  row: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: tokens.spacingHorizontalM,
    paddingTop: tokens.spacingVerticalM, paddingBottom: tokens.spacingVerticalM },
});

function Workspace({ dark, setDark }) {
  const styles = useStyles();
  const [tab, setTab] = React.useState('brief');
  const [open, setOpen] = React.useState(false);
  const [email, setEmail] = React.useState('');
  const [permission, setPermission] = React.useState('Can view');
  const [error, setError] = React.useState('');
  const [notice, setNotice] = React.useState('');
  const [people, setPeople] = React.useState([{ email: 'alex@example.com', permission: 'Owner' }]);
  function submit(event) {
    event.preventDefault();
    const value = email.trim().toLowerCase();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      setError('Enter an email address such as name@example.com'); return;
    }
    if (people.some(person => person.email === value)) {
      setError('This address already appears in the local access list'); return;
    }
    setPeople(previous => [...previous, { email: value, permission }]);
    setNotice(`Added ${value} as ${permission.toLowerCase()} in this local example. No invitation was sent.`);
    setEmail(''); setError(''); setOpen(false); setTab('people');
  }
  return <main className={styles.page}>
    <div className={styles.header}>
      <Title3 as="h1">Launch workspace</Title3>
      <Button onClick={() => setDark(!dark)}>{dark ? 'Light theme' : 'Dark theme'}</Button>
    </div>
    <Body1>Project brief and access settings · local demonstration</Body1>
    <TabList aria-label="Workspace views" selectedValue={tab}
      onTabSelect={(_, data) => setTab(data.value)}>
      <Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
      <Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
    </TabList>
    <section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
      hidden={tab !== 'brief'} className={styles.panel} tabIndex={0}>
      <h2>Launch a clearer onboarding flow</h2>
      <p>Review the welcome screen and first-project setup before the team walkthrough.</p>
      <p>Decision needed: should the sample project be created automatically or offered as an option?</p>
    </section>
    <section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
      hidden={tab !== 'people'} className={styles.panel} tabIndex={0}>
      <h2>People with access</h2>
      <ul>{people.map(person => <li key={person.email} className={styles.row}>
        <span>{person.email}</span><strong>{person.permission}</strong>
      </li>)}</ul>
    </section>
    <Dialog open={open} onOpenChange={(_, data) => setOpen(data.open)}>
      <DialogTrigger disableButtonEnhancement><Button appearance="primary">Add a person</Button></DialogTrigger>
      <DialogSurface>
        <form noValidate onSubmit={submit}>
          <DialogBody>
            <DialogTitle>Add a person to Launch workspace</DialogTitle>
            <DialogContent className={styles.form}>
              <p>This example updates local state only. It does not send email or change real access.</p>
              <Field label="Email address" required validationState={error ? 'error' : 'none'}
                validationMessage={error || undefined}>
                <Input type="email" value={email} onChange={(_, data) => { setEmail(data.value); setError(''); }} />
              </Field>
              <Field label="Permission" hint="Viewers can read; editors can change the brief in a real application">
                <Select value={permission} onChange={(_, data) => setPermission(data.value)}>
                  <option>Can view</option><option>Can edit</option>
                </Select>
              </Field>
            </DialogContent>
            <DialogActions>
              <DialogTrigger disableButtonEnhancement><Button type="button">Cancel</Button></DialogTrigger>
              <Button type="submit" appearance="primary">Add locally</Button>
            </DialogActions>
          </DialogBody>
        </form>
      </DialogSurface>
    </Dialog>
    <p role="status">{notice}</p>
  </main>;
}
function App() {
  const [dark, setDark] = React.useState(false);
  return <FluentProvider theme={dark ? webDarkTheme : webLightTheme}>
    <Workspace dark={dark} setDark={setDark} />
  </FluentProvider>;
}
createRoot(document.getElementById('root')).render(<App />);
```

### 프로젝트를 위한 프롬프트

```
@fluentui/react-components의 Fluent UI React v9를 사용하세요. webLightTheme을 적용한 FluentProvider를 렌더링하고 webDarkTheme 전환 기능을 제공하세요. Title3와 Body1로 설명을 구성하고, 명시적 패널이 있는 제어형 TabList/탭 보기를 사용하세요. Field/Input/Select와 실제 폼을 포함한 접근 권한 Dialog를 구성하세요. 사용자 지정 표면과 간격에는 토큰을 사용하세요. 제공된 컴포넌트 예시와 런타임 타입 쌍을 따르되, Subtitle1은 20/28을 사용하세요. 유효하지 않은 입력은 보존하고, 로컬 목록의 중복 주소는 거부하며, Dialog를 닫을 때 포커스를 복원하고, 로컬에서 추가되는 항목을 정확히 설명하세요. 권한 부여 방식과 결과 계약이 제공된 경우에만 공유 서비스를 연결하세요.
```

## Microsoft Fluent 2 디자인 시스템 출처 및 범위

Microsoft 제품이나 실시간 공유 서비스가 아닌, Fluent UI React v9를 살펴보는 OpenDesign 애플리케이션 예시입니다. 독립형 미리보기는 검증된 웹 토큰과 구성 요소 구조를 HTML/CSS 방식으로 해석한 것이며, 아래 React 코드는 네이티브 Fluent 구성 요소를 사용합니다. 참조 값은 2026년 10월 3일에 확인한 링크된 소스 스냅샷을 기준으로 합니다. 모든 릴리스, 플랫폼 또는 브랜드에서 값이 동일하다는 보장은 아닙니다.

- React v9 패키지 진입점 Provider 및 스위트 설치

- 테마 아키텍처 시맨틱 토큰 사용 및 사용 가능한 테마

- 런타임 타이포그래피 글꼴 스택, 크기, 줄 높이 및 굵기

- 텍스트 스타일 매핑 전역 토큰에 매핑된 프리셋

- 디자인 타이포그래피 단계 디자인 언어 표. Subtitle1은 런타임 쌍과 다릅니다.

- 런타임 간격 가로 및 세로 토큰 이름

- 레이아웃 지침 전역 간격 및 반응형 고려 사항

- 모서리 반경 토큰 사용 가능한 런타임 형태 값

- 모션 지속 시간 런타임 지속 시간 값

- 라이트 시맨틱 매핑 중립색 및 브랜드 별칭 할당

- 다크 시맨틱 매핑 다크 테마 별칭 할당

- 브랜드 색상 단계 실제 웹 및 Teams 브랜드 값

- 웹 테마 구성 webLightTheme은 brandWeb을 사용합니다.

- Field 사용법 레이블, 유효성 검사 및 구성 지침

- Field API 슬롯 및 접근성 있는 자식 요소 계약

- TabList 사용법 레이아웃 및 카테고리 선택 지침

- TabList API 제어형 선택 및 포커스 옵션

- Dialog 사용법 폼 배치 및 모달 상호작용 지침

- Dialog API 제어형 표시 및 모달 유형

## Microsoft Fluent 2 디자인 시스템 관련 자료

### Monday Vibe

Vibe 4 토큰과 React 컨트롤로 작동하는 배송 보드를 구성하세요. 의미 기반 작업 색상과 보드 라벨의 차이, 명명된 간격 단계, 글꼴 축약 표기, 새 애플리케이션에 필요한 명시적인 CSS 설정을 알아보세요.

### Morningstar 디자인 시스템

숫자를 정렬하고, 자산 색상의 의미를 유지하며, 읽기 쉬운 타이포그래피를 적용한 리서치 인터페이스를 구성하세요. MDS 상수, Vue 3 도입, Morningstar Intrinsic 전환, 정렬 가능한 가상 자산 배분 노트를 살펴보세요.

### Mozilla Protocol

실제 글꼴 스택과 간격 및 컨테이너 크기를 다루는 Protocol 편집 가이드, 반응형 삽화가 포함된 읽기 페이지, 완전한 기본 CSS 폼 예제를 살펴보세요.


- [구현 레퍼런스](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-components/README.md)
- [출처](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/lightColor.ts)
- [출처](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/brandColors.ts)
- [출처](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/spacings.ts)
- [출처](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts)
- [출처](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/borderRadius.ts)
- [출처](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/durations.ts)
- [출처](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/darkColor.ts)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-components/README.md)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/docs/architecture/design-tokens.md)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/lightColor.ts)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/darkColor.ts)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/typographyStyles.ts)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/spacings.ts)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/borderRadius.ts)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-tabs/library/src/components/TabList/TabList.types.ts)
- [지원 사양 읽기](https://fluent2.microsoft.design/components/web/react/core/tablist/usage)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-field/library/src/components/Field/Field.types.ts)
- [지원 사양 읽기](https://fluent2.microsoft.design/components/web/react/core/field/usage)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-dialog/library/src/components/Dialog/Dialog.types.ts)
- [지원 사양 읽기](https://fluent2.microsoft.design/components/web/react/core/dialog/usage)
- [지원 사양 읽기](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/durations.ts)
- [코드 참조](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-components/README.md)
- [코드 참조](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-field/library/src/components/Field/Field.types.ts)
- [코드 참조](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-tabs/library/src/components/TabList/TabList.types.ts)
- [React v9 패키지 진입점 Provider 및 스위트 설치](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-components/README.md)
- [테마 아키텍처 시맨틱 토큰 사용 및 사용 가능한 테마](https://github.com/microsoft/fluentui/blob/master/docs/architecture/design-tokens.md)
- [런타임 타이포그래피 글꼴 스택, 크기, 줄 높이 및 굵기](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts)
- [텍스트 스타일 매핑 전역 토큰에 매핑된 프리셋](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/typographyStyles.ts)
- [디자인 타이포그래피 단계 디자인 언어 표. Subtitle1은 런타임 쌍과 다릅니다.](https://fluent2.microsoft.design/typography)
- [런타임 간격 가로 및 세로 토큰 이름](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/spacings.ts)
- [레이아웃 지침 전역 간격 및 반응형 고려 사항](https://fluent2.microsoft.design/layout)
- [모서리 반경 토큰 사용 가능한 런타임 형태 값](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/borderRadius.ts)
- [모션 지속 시간 런타임 지속 시간 값](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/durations.ts)
- [라이트 시맨틱 매핑 중립색 및 브랜드 별칭 할당](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/lightColor.ts)
- [다크 시맨틱 매핑 다크 테마 별칭 할당](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/darkColor.ts)
- [브랜드 색상 단계 실제 웹 및 Teams 브랜드 값](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/brandColors.ts)
- [웹 테마 구성 webLightTheme은 brandWeb을 사용합니다.](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/themes/web/lightTheme.ts)
- [Field 사용법 레이블, 유효성 검사 및 구성 지침](https://fluent2.microsoft.design/components/web/react/core/field/usage)
- [Field API 슬롯 및 접근성 있는 자식 요소 계약](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-field/library/src/components/Field/Field.types.ts)
- [TabList 사용법 레이아웃 및 카테고리 선택 지침](https://fluent2.microsoft.design/components/web/react/core/tablist/usage)
- [TabList API 제어형 선택 및 포커스 옵션](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-tabs/library/src/components/TabList/TabList.types.ts)
- [Dialog 사용법 폼 배치 및 모달 상호작용 지침](https://fluent2.microsoft.design/components/web/react/core/dialog/usage)
- [Dialog API 제어형 표시 및 모달 유형](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-dialog/library/src/components/Dialog/Dialog.types.ts)
