Skip to content

Commit 26c670a

Browse files
authored
feat(react-headless-components-preview): scaffold the package and add first components (#35931)
1 parent c78d223 commit 26c670a

83 files changed

Lines changed: 1602 additions & 2 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ packages/react-components/component-selector-preview/library @microsoft/teams-pr
331331
packages/react-components/component-selector-preview/stories @microsoft/teams-prg
332332
packages/react-components/react-menu-grid-preview/library @microsoft/teams-prg
333333
packages/react-components/react-menu-grid-preview/stories @microsoft/teams-prg
334+
packages/react-components/react-headless-components-preview/library @microsoft/cxe-prg
335+
packages/react-components/react-headless-components-preview/stories @microsoft/cxe-prg
334336
# <%= NX-CODEOWNER-PLACEHOLDER %>
335337

336338
# Deprecated v9 packages - exposed as part of `/unstable` api

apps/pr-deploy-site/just.config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import path from 'path';
33
import { series, task, copyInstructionsTask, copyInstructions, cleanTask } from '@fluentui/scripts-tasks';
44
import { findGitRoot, getAllPackageInfo } from '@fluentui/scripts-monorepo';
55

6+
function getDeployDirectoryName(packageName: string) {
7+
return packageName.replace(/^@[^/]+\//, '');
8+
}
9+
610
task('clean', cleanTask());
711

812
const gitRoot = findGitRoot();
@@ -29,6 +33,7 @@ const dependencies = [
2933
'@fluentui/public-docsite-v9',
3034
'@fluentui/perf-test-react-components',
3135
'@fluentui/theme-designer',
36+
'@fluentui/react-headless-components-preview-stories',
3237
// web-components
3338
'@fluentui/web-components',
3439
// charting
@@ -45,7 +50,10 @@ repoDeps.forEach(dep => {
4550

4651
if (fs.existsSync(packageDist)) {
4752
instructions.push(
48-
...copyInstructions.copyFilesInDirectory(packageDist, path.join('dist', path.basename(dep.packagePath))),
53+
...copyInstructions.copyFilesInDirectory(
54+
packageDist,
55+
path.join('dist', getDeployDirectoryName(dep.packageJson.name)),
56+
),
4957
);
5058
deployedPackages.add(dep.packageJson.name);
5159
}

apps/pr-deploy-site/pr-deploy-site.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ function main() {
8484
icon: 'CheckMark',
8585
title: 'Theme Designer v9',
8686
},
87+
{
88+
package: '@fluentui/react-headless-components-preview-stories',
89+
link: './react-headless-components-preview-stories/storybook/index.html',
90+
icon: 'Code',
91+
title: '@fluentui/react-headless-components-preview Storybook',
92+
},
8793
{
8894
package: '@fluentui/perf-test',
8995
link: './perf-test/index.html',
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$schema": "https://json.schemastore.org/swcrc",
3+
"exclude": [
4+
"/testing",
5+
"/**/*.cy.ts",
6+
"/**/*.cy.tsx",
7+
"/**/*.spec.ts",
8+
"/**/*.spec.tsx",
9+
"/**/*.test.ts",
10+
"/**/*.test.tsx"
11+
],
12+
"jsc": {
13+
"parser": {
14+
"syntax": "typescript",
15+
"tsx": true,
16+
"decorators": false,
17+
"dynamicImport": false
18+
},
19+
"externalHelpers": true,
20+
"transform": {
21+
"react": {
22+
"runtime": "classic",
23+
"useSpread": true
24+
}
25+
},
26+
"target": "es2022"
27+
},
28+
"minify": false,
29+
"sourceMaps": true
30+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@fluentui/react-headless-components-preview
2+
3+
Copyright (c) Microsoft Corporation
4+
5+
All rights reserved.
6+
7+
MIT License
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14+
15+
Note: Usage of the fonts and icons referenced in Fluent UI React is subject to the terms listed at https://aka.ms/fluentui-assets-license
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# @fluentui/react-headless-components-preview
2+
3+
**React Headless Components for [Fluent UI React](https://react.fluentui.dev/)**
4+
5+
> [!WARNING] > **This package is in preview and not production-ready.** APIs may change without notice before final release. **Do not use in production.**
6+
>
7+
> This package exposes unstyled, headless Fluent UI v9 primitives for teams building custom design systems. For most teams, [`@fluentui/react-components`](https://www.npmjs.com/package/@fluentui/react-components) remains the recommended default.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as rhc from '@fluentui/react-headless-components-preview';
2+
3+
console.log(rhc);
4+
5+
export default {
6+
name: 'react-headless-components-preview: entire library',
7+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3+
"extends": "@fluentui/scripts-api-extractor/api-extractor.common.v-next.json",
4+
"mainEntryPointFilePath": "<projectRoot>/../../../../../../dist/out-tsc/types/packages/react-components/<unscopedPackageName>/library/src/index.d.ts"
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/** Jest test setup file. */
2+
3+
require('@testing-library/jest-dom');
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# @fluentui/react-headless-components Spec
2+
3+
## Background
4+
5+
`@fluentui/react-headless-components` is an **advanced, opt-in** package that exposes the headless layer of Fluent UI v9 components: pure component logic, accessibility patterns, and semantic slot structure — without any styling opinions.
6+
7+
It is intended for teams building custom design systems that significantly diverge from Fluent 2. For most teams, the default styled components in `@fluentui/react-components` remain the recommended path.
8+
9+
**What this package provides:**
10+
11+
- Unstyled primitive components (headless components)
12+
- Component behavior, structure, and ARIA patterns
13+
- Keyboard handling
14+
- Semantic slot structure
15+
- Building blocks for advanced composition: `use{Component}` and `render{Component}`
16+
- Optional context-value hooks for compound components (for example, `useAccordionContextValues`)
17+
18+
**What this package does NOT provide:**
19+
20+
- Design props (`appearance`, `size`, `shape`, etc.)
21+
- Style logic (Griffel, design tokens)
22+
- Motion logic (animations, transitions)
23+
- Default slot implementations (icons, components)
24+
25+
> **Important:** Base hooks provide ARIA attributes and semantic structure, but not visual accessibility (e.g., focus indicators, sufficient contrast). Consumers are responsible for implementing these in their custom styles.
26+
27+
## Prior Art
28+
29+
- [RFC: Component Base State Hooks](../../../../../../docs/react-v9/contributing/rfcs/react-components/convergence/base-state-hooks.md)
30+
- Fluent UI v9 `_unstable` hook convention used throughout `@fluentui/react-*` packages
31+
32+
## Sample Code
33+
34+
Building a fully custom button using base hooks:
35+
36+
```tsx
37+
import * as React from 'react';
38+
import { useButton, renderButton } from '@fluentui/react-headless-components';
39+
import type { ButtonProps, ButtonState } from '@fluentui/react-headless-components';
40+
41+
type CustomButtonProps = ButtonProps & {
42+
variant?: 'primary' | 'secondary' | 'tertiary';
43+
tone?: 'neutral' | 'success' | 'warning' | 'danger';
44+
};
45+
46+
export const CustomButton = React.forwardRef<HTMLButtonElement, CustomButtonProps>(
47+
({ variant = 'primary', tone = 'neutral', ...props }, ref) => {
48+
const state = useButton(props, ref);
49+
50+
state.root.className = ['custom-btn', `custom-btn--${variant}`, `custom-btn--${tone}`, state.root.className]
51+
.filter(Boolean)
52+
.join(' ');
53+
54+
if (state.icon) {
55+
state.icon.className = ['custom-btn__icon', state.icon.className].filter(Boolean).join(' ');
56+
}
57+
58+
return renderButton(state as ButtonState);
59+
},
60+
);
61+
```
62+
63+
## API
64+
65+
### Naming Conventions
66+
67+
| Artifact | Pattern | Example |
68+
| ---------- | ------------------------ | -------------- |
69+
| Primitive | `${ComponentName}` | `Button` |
70+
| Hook | `use${ComponentName}` | `useButton` |
71+
| Props type | `${ComponentName}Props` | `ButtonProps` |
72+
| State type | `${ComponentName}State` | `ButtonState` |
73+
| Render fn | `render${ComponentName}` | `renderButton` |
74+
75+
Public exports in this package use stable names and wrap internal `_unstable` base hooks from individual component packages.
76+
77+
### Type Hierarchy
78+
79+
```tsx
80+
// Package types are the headless/base component contracts
81+
type ButtonProps = ComponentProps<ButtonSlots> & {
82+
disabled?: boolean;
83+
disabledFocusable?: boolean;
84+
iconPosition?: 'before' | 'after';
85+
};
86+
87+
type ButtonState = ComponentState<ButtonSlots> & {
88+
disabled: boolean;
89+
disabledFocusable: boolean;
90+
iconPosition: 'before' | 'after';
91+
iconOnly: boolean;
92+
};
93+
```
94+
95+
### Exported Components
96+
97+
Each exported component is available as an unstyled primitive component, with its low-level building blocks for advanced composition.
98+
99+
#### Accordion family
100+
101+
- `Accordion`, `AccordionItem`, `AccordionHeader`, `AccordionPanel` (unstyled primitives)
102+
- `useAccordion`, `useAccordionItem`, `useAccordionHeader`, `useAccordionPanel`
103+
- `renderAccordion`, `renderAccordionItem`, `renderAccordionHeader`, `renderAccordionPanel`
104+
- Context hooks for advanced composition: `useAccordionContext`, `useAccordionContextValues`
105+
106+
#### Button
107+
108+
- `Button` (unstyled primitive)
109+
- `useButton`
110+
- `renderButton`
111+
112+
#### Divider
113+
114+
- `Divider` (unstyled primitive)
115+
- `useDivider`
116+
- `renderDivider`
117+
118+
## Structure
119+
120+
### Composition Layers
121+
122+
```text
123+
use{Component}Base_unstable (internal base state hook — logic + accessibility)
124+
125+
use{Component} (public stable hook in this package)
126+
127+
render{Component} (public render function in this package)
128+
129+
{Component} (unstyled primitive component in this package)
130+
```
131+
132+
This package exposes headless primitives and their building blocks. Styled components in `@fluentui/react-components` continue to compose on top of the same base logic.
133+
134+
### Public API
135+
136+
Every exported component exposes:
137+
138+
- an unstyled primitive component (`Button`)
139+
- a stable hook (`useButton`)
140+
- a render function (`renderButton`)
141+
- optional context-value hooks for compound patterns (`useAccordionContextValues`)
142+
143+
### Internal
144+
145+
Each component's base logic lives in its individual package (for example, `@fluentui/react-button`). This package re-exports stable wrappers and primitives on top of that base logic.
146+
147+
## Migration
148+
149+
This package is a new addition; there is no migration from v8 or v0. For teams currently using full Fluent UI components that want to adopt base hooks, the path is:
150+
151+
1. Replace `useButton_unstable(props, ref)` with `useButton(props, ref)` from this package
152+
2. Remove design props (`appearance`, `size`, `shape`) from your props type and use `ButtonProps` from this package
153+
3. Apply your own class names or styles to the returned state slots before passing to the render function
154+
155+
## Behaviors
156+
157+
Headless hooks encapsulate interactive behavior inherited by the styled component layer:
158+
159+
### Component States
160+
161+
- **Disabled**: ARIA `disabled` and `aria-disabled` attributes set; keyboard events suppressed where appropriate
162+
- **DisabledFocusable**: Element remains focusable while being semantically disabled (`aria-disabled="true"`)
163+
- **Expanded / collapsed**: Accordion primitives manage disclosure state and relationships via ARIA
164+
165+
### Interaction
166+
167+
#### Keyboard
168+
169+
Keyboard behavior is component-specific and follows WAI-ARIA authoring practices. Each hook applies the same keyboard handling as the corresponding styled component.
170+
171+
#### Cursor
172+
173+
No cursor styles are applied by base hooks. Consumers are responsible for setting appropriate cursor styles.
174+
175+
#### Touch
176+
177+
Touch events are handled via the same event handlers applied to root slots.
178+
179+
#### Screen readers
180+
181+
- ARIA roles, states, and properties are applied by the hooks
182+
183+
## Accessibility
184+
185+
Headless hooks and primitives provide the semantic foundation for accessibility, but consumers must ensure their custom styles maintain:
186+
187+
- **Visible focus indicators** — base hooks do not apply focus ring styles
188+
- **Sufficient color contrast** — base hooks do not apply colors or tokens
189+
- **Appropriate visual feedback** for all interactive states (hover, active, disabled)
190+
191+
### ARIA Patterns Applied
192+
193+
Each component follows its corresponding WAI-ARIA authoring practice:
194+
195+
| Component | ARIA pattern |
196+
| --------- | --------------------- |
197+
| Accordion | Accordion pattern |
198+
| Button | Button / Link pattern |
199+
| Divider | Separator pattern |
200+
201+
> Keyboard navigation, focus management, and state announcements are delegated to the individual component packages and are identical to their styled counterparts.

0 commit comments

Comments
 (0)