- name
- headless-component
- description
- Author or extend unstyled Fluent UI v9 primitives in @fluentui/react-headless-components-preview, including stable hooks, render functions, state data attributes, compound context wiring, tests, exports, and CSS Module stories. Use for any change that adds a headless component or component family to the preview library.
- argument-hint
- <ComponentName>
- disable-model-invocation
- true
- allowed-tools
- Bash Read Write Edit Grep Glob
# Author a Headless Component
Author **$ARGUMENTS** in:
- `packages/react-components/react-headless-components-preview/library`
- `packages/react-components/react-headless-components-preview/stories`
Headless components provide Fluent behavior, accessibility, semantic slots, and
stable state selectors without Fluent styling. They must remain usable with plain
CSS, CSS Modules, Tailwind, or another styling system.
If `$ARGUMENTS` is empty or is not a PascalCase component name, ask for a valid
component name before searching or editing.
## Source Of Truth And References
This skill is the operational source of truth for authoring a headless
component. Do not require the RFC to be loaded for routine work: the normative
package contract, state-attribute rules, packaging requirements, stacked-PR
workflow, and validation bar are fully stated here.
Always inspect current code before editing:
1. The corresponding styled v9 package under
`packages/react-components/react-<name>/library/src/`
2. The nearest headless implementation with the same shape:
- simple primitive: `library/src/components/Button/`
- compound primitive using upstream base hooks:
`library/src/components/Accordion/`
- composite/context/positioning primitive: `library/src/components/Menu/`
3. The current package manifests, export barrels, API reports, bundle fixtures,
tests, and stories touched by the change
Consult these documents only when changing the package-wide contract, resolving
an ambiguity not answered here, or updating their documentation:
- `packages/react-components/react-headless-components-preview/library/docs/Spec.md`
- `packages/react-components/react-headless-components-preview/stories/README.md`
- `docs/react-v9/contributing/rfcs/react-components/convergence/headless-components.md`
The RFC provides decision history and rationale; it is not a per-component
checklist and may describe the intended stable package name rather than the
current preview package.
Use current implementations as structural references, not as permission to copy
legacy violations. Repository instructions override existing code.
## Preflight
Before writing code:
1. Search the headless library and stories for the component and its family.
Extend an existing implementation instead of creating a competing one.
2. Inspect the corresponding styled v9 package and its public API docs. Inventory:
- public components and subcomponents
- slots, props, state, refs, and polymorphic `as` behavior
- controlled and uncontrolled state
- context values
- ARIA semantics and keyboard/focus behavior
- positioning, portals, and mount-node behavior
3. Verify the upstream package's actual exports before reimplementing logic.
Search for `use<Name>Base_unstable`, `render<Name>_unstable`,
`use<Name>ContextValues_unstable`, context hooks, and shared utilities.
4. Classify the work:
- **simple** — one hook, renderer, component, and state attributes
- **compound** — parent plus child primitives and context-value wiring
- **composite** — substantial focus, selection, positioning, portal, or
nested-component behavior
5. Define the stable `data-*` contract before implementation. Include only
behavior/base state, never visual design state.
6. Record every inventory row as implemented, intentionally excluded with a
source-backed reason, or unresolved. Trace implemented rows through:
`types/exports -> hook/context -> rendered DOM/slots -> test or story`.
7. Plan the change as three stacked PRs before editing:
- **bottom PR — regression test:** lock down the existing behavior that the
base extraction must preserve; do not change production code
- **middle PR — component package:** base hook/types/renderer/context exports
and their package-local tests, API docs, bundle-size coverage, manifest
updates, and change file
- **top PR — headless package:** stable headless primitive, `data-*`
contract, headless tests, stories, public subpath, headless bundle-size
coverage, API docs, manifest updates, and change file
Do not mix component-package and headless-package changes in one PR. The
headless layer depends on the base APIs introduced or confirmed by the component
package, so the component-package PR is always below the headless PR.
Do not broaden the component API with styled-only props such as `appearance`,
`size`, `shape`, or other visual variants.
Use an existing repository generator for the component family when one is
available. Otherwise, follow the file structure and validation requirements
below rather than creating a parallel scaffold.
## Package Contract
Every public headless primitive exposes stable, unsuffixed names:
```text
Component
useComponent
renderComponent
ComponentProps
ComponentSlots
ComponentState
```
Compound families additionally expose their public children, context types, and
context hooks when consumers need them for advanced composition.
Internally, these APIs normally wrap the corresponding package's `_unstable`
base hook and renderer:
```text
useComponentBase_unstable
↓
useComponent
↓
renderComponent
↓
Component
```
The headless layer must not add styling, design props, motion, default icons, or
new interaction behavior.
## File Structure
For a simple component:
```text
library/src/components/ComponentName/
ComponentName.tsx
ComponentName.types.ts
ComponentName.test.tsx
index.ts
renderComponentName.ts
useComponentName.ts
library/src/component-name.ts
stories/src/ComponentName/
ComponentNameDescription.md
ComponentNameDefault.stories.tsx
component-name.module.css
index.stories.tsx
```
For a compound family, place child primitives beneath the root folder:
```text
library/src/components/ComponentName/
ComponentName.tsx
...
ComponentPart/
ComponentPart.tsx
ComponentPart.types.ts
ComponentPart.test.tsx
index.ts
renderComponentPart.ts
useComponentPart.ts
```
Add `*.cy.tsx` integration coverage when behavior depends on real browser focus,
Tabster, positioning, portals, native popovers, or nested trigger composition.
## Implementation
### Component
- Add `'use client';` to component and hook modules that use React client APIs.
- Use `ForwardRefComponent<Props>` with `React.forwardRef` for ref-capable
primitives. Never use `React.FC`.
- For provider-only components with no meaningful DOM ref, use a normally typed
function returning `JSXElement`; do not invent a fake ref and do not use
`React.FC`.
- Keep the component thin: state hook, optional context-values hook, renderer.
- Set `displayName` on every exported React component.
- Add concise JSDoc describing purpose and any required accessibility contract.
- Do not import icons or add default icons in base hooks or headless renderers.
If an icon is needed in a styled wrapper or story, import it there and define
`bundleIcon` at module scope, never inside a hook or render function.
```tsx
'use client';
import * as React from 'react';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
export const ComponentName: ForwardRefComponent<ComponentNameProps> = React.forwardRef((props, ref) => {
const state = useComponentName(props, ref);
return renderComponentName(state);
});
ComponentName.displayName = 'ComponentName';
```
For compound roots, derive and pass context values between the state hook and
renderer.
### Hook
- Prefer the upstream `useComponentNameBase_unstable` hook.
- When adding a new v9 base hook, follow the repository-enforced signature:
`useComponentNameBase_unstable(props, ref?)`, with an explicitly typed
`props` parameter and `ref` typed as `React.Ref<...>`.
- Keep base hooks free of Tabster and other forbidden focus/keyboard runtimes.
Logic requiring those runtimes stays in the styled package's wrapping
`useComponentName_unstable` hook. Run the component-package lint target so
`base-hook-signature` and `base-hook-no-forbidden-runtime` validate the split.
- Preserve upstream behavior rather than copying the styled component's main
hook, which may include design defaults and styling concerns.
- Reuse exported context-value hooks and utilities. Do not reconstruct their
return objects manually.
- Reimplement behavior only after proving the required base API is not exported.
In that case, port only behavior/accessibility logic and preserve parity with
the styled package's tests.
- Use `useFluent_unstable()` for `targetDocument` and its `defaultView`; never
access `window`, `document`, or `navigator` directly.
### Stable `data-*` Attributes
State attributes are the supported CSS targeting contract.
- Boolean and presence state: emit `''` when true and omit when false.
Reserve explicit string values for enum or tri-state contracts.
- Enum or tri-state: emit the documented string value.
- Put attributes on the root slot unless a documented exception makes another
slot the semantic state owner.
- Type every attribute on `ComponentState` so it appears in generated API docs.
- Apply reserved attributes after the base hook resolves consumer root props.
The headless state must win if a consumer supplies a conflicting reserved
attribute.
- Removal or rename is breaking. Additions require tests and documentation.
Use `toDataAttributeValue` from `library/src/utils` for presence and string
attributes:
```tsx
const state: ComponentNameState = useComponentNameBase_unstable(props, ref);
// Applied after consumer props so base state cannot be misrepresented.
state.root['data-disabled'] = toDataAttributeValue(state.disabled);
return state;
```
Good attributes describe behavior: `data-disabled`, `data-open`,
`data-expanded`, `data-checked`, `data-selected`, `data-orientation`,
`data-icon-only`.
Do not expose design attributes such as `data-appearance`, `data-size`,
`data-shape`, or styling-specific class names.
### Types
- Derive props, slots, and state from the upstream base types, not the styled
public types when those include design props.
- Alias unchanged base types directly.
- Extend state only for public headless additions such as typed `data-*`
attributes.
- Keep exact attribute value unions when known instead of widening to `string`.
- Export event and context types required by consumers of compound components.
- Do not use `any` or unnecessary type assertions to hide an API mismatch.
### Renderer and Context
- Re-export or alias the upstream unstable renderer when its slot structure is
already correct.
```ts
export { renderComponentName_unstable as renderComponentName } from '@fluentui/react-component';
```
- Compound renderers must provide the same behavior contexts as the styled
component while omitting style contexts.
- Keep portal and positioning behavior semantic. Inline rendering is allowed
only when it is the intended headless contract, not as a test shortcut.
## Public Exports
### Component-Package PR
When the headless primitive needs new base APIs from the corresponding
`@fluentui/react-*` component package, update all applicable surfaces in the
middle PR:
1. Export base types from the component implementation barrel, for example
`ComponentBaseProps`, `ComponentBaseState`, and the existing slots type.
2. Export `useComponentBase_unstable` from the component implementation barrel.
3. Export the renderer and context/context-values hooks needed by the headless
composition. Reuse existing exports; do not create a second equivalent API.
4. Re-export every new public symbol from the component package root
`library/src/index.ts`.
5. Run `generate-api` and commit the updated `library/etc/*.api.md` files.
6. Update the component package `library/package.json` when runtime imports or
public entry points change:
- add every new runtime package to `dependencies`
- use the version range already established for that dependency in the
workspace; when it is new, derive it from the dependency package's current
`package.json` rather than inventing a range
- add type-only tooling/test packages to the existing appropriate dependency
section only when the repository convention requires it
- keep dependency keys alphabetized
- do not add headless-package dependencies to a component package
- do not introduce dependencies between Tier 3 component packages; move
genuinely shared behavior to the appropriate utility/shared-context layer
- preserve the existing root export map unless adding an intentional new
package subpath
- if a new subpath is added, add matching `types`, `node`, `import`, and
`require` entries and ensure its built files are included by `files`
- do not hand-edit package versions
7. If `package.json` changes, run Yarn through the repository workflow and
commit `yarn.lock` only when Yarn updates it. Never hand-edit `yarn.lock`.
Adding only a base hook/type export to an existing root entry normally does not
require a new `package.json` export-map entry. It does require root barrel and
generated API updates.
### Headless-Package PR
Update every required surface in the top PR:
1. `library/src/components/ComponentName/index.ts`
2. `library/src/component-name.ts`
3. Parent-family indexes for all public subcomponents, types, events, contexts,
hooks, and renderers
4. `library/package.json`:
- add the corresponding `@fluentui/react-*` package to `dependencies` if it
is not already present
- add any other new runtime dependency used by library source
- use the dependency version range established elsewhere in the workspace;
for a newly referenced workspace package, derive it from that package's
current version
- keep dependency keys alphabetized
- add `exports["./component-name"]` with matching `types`, `node`, `import`,
and `require` paths
- keep component subpath export keys alphabetized, with
`"./package.json"` retained in the package's established position
- preserve `exports["./package.json"]`
- confirm the generic `files` entries still publish the generated
declaration and JavaScript files; update `files` only if the new artifact
falls outside those patterns
- preserve `sideEffects: false` only while the new modules remain free of
import-time side effects
- do not add story-only packages, icons, or styling dependencies to the
library manifest
- do not hand-edit the package version
5. Run `generate-api` and commit the new or updated `library/etc/component-name.api.md`
plus any aggregate API report changed by the export.
6. If `package.json` changes, run Yarn through the repository workflow and
commit `yarn.lock` only when Yarn updates it. Never hand-edit `yarn.lock`.
The stories package is private and normally needs no `package.json` change.
Change it only when the story runtime truly needs a new direct dependency or
script; do not duplicate dependencies that are already supplied by the
workspace's established Storybook setup.
The package intentionally uses kebab-case subpath exports. Keep
`library/src/index.ts` unchanged unless the package's root-export policy changes.
The colocated conformance helper verifies the top-level source file and export
map for components that run `isConformant`.
## Bundle-Size Requirements
Bundle-size coverage is required in both PR layers and must be reviewable in the
PR that introduces the relevant public API.
### Component-Package PR
- Inspect `packages/react-components/react-<name>/library/bundle-size/`.
- Bundle fixtures are component-level. For a newly public styled component, add
a focused
`<ComponentName>.fixture.js` that imports the component from the package root,
references it (normally with `console.log`), and exports a stable fixture
name.
- Do not create standalone fixtures for base hooks, renderers, context helpers,
or type-only exports.
- For a new public member of an existing family, add a fixture when it can be
consumed independently or when the existing family fixture does not import
it. Do not assume a nearby fixture covers a new export.
- Type-only exports do not need a bundle fixture because they are erased.
- Keep fixtures focused on public imports. Never import private source paths to
make a bundle check pass.
- Run the component package's Nx `bundle-size` target and inspect the generated
report for unexpected growth.
### Headless-Package PR
Update
`library/bundle-size/AllComponents.fixture.js` for every new public component
subpath:
1. Add `import * as ComponentName from
'@fluentui/react-headless-components-preview/component-name';`.
2. Add `ComponentName` to the logged object.
3. Keep both lists alphabetized and exactly in sync.
The namespace import intentionally measures the complete public subpath:
component, hook, renderer, types erased at compile time, and compound-family
exports. A `package.json` subpath without a matching fixture entry is
incomplete, even when build and tests pass.
Run:
```bash
yarn nx run react-<name>:bundle-size
yarn nx run react-headless-components-preview:bundle-size
```
Use `yarn nx show project <project>` to confirm the component project name.
Commit source fixtures, not generated `dist/bundle-size` or
`dist/bundle-analyze` output.
## Tests
Add focused tests beside every public primitive.
### Required Coverage
- `isConformant` for every ref-capable component
- default semantic render and native element behavior
- ref, `className`, `style`, `as`, and slot forwarding where applicable
- every stable `data-*` attribute:
- positive state
- false/absent state
- exact string value for enum/tri-state attributes
- consumer override cannot misrepresent reserved state
- ARIA role, state, relationships, and accessible name
- controlled and uncontrolled behavior
- primary interaction and its inverse: open/close, expand/collapse,
select/deselect, check/uncheck
- compound context and parent-child coordination
- disabled and disabled-focusable behavior
- keyboard and focus behavior at the appropriate unit or Cypress layer
Prefer Testing Library role/name queries and assert observable behavior. A count
or snapshot alone does not prove the contract.
Use the real browser Cypress layer for behavior that jsdom cannot validate
reliably, such as focus restoration, arrow navigation, Escape dismissal,
positioning, native popovers, and portal composition.
## Stories
Stories demonstrate that the unstyled primitive can support an independent
design system. Styling belongs only in the stories package.
1. Import from the public package subpath, never a relative library path.
2. Add a short, behavior-accurate `ComponentNameDescription.md`.
3. Put metadata and exports in `index.stories.tsx`.
4. Import every CSS Module used by the story family from a story or
`index.stories.tsx`, and import the primary module in `index.stories.tsx` so
the custom "Show code" panel includes the CSS source. No manual source-panel
configuration is needed.
5. Include the component family as `subcomponents` for compound APIs.
6. Add a default story plus stories for important behavioral states.
7. Exercise the primary interaction instead of rendering only static variants.
Keep stories demonstrable and cover the resulting role, ARIA, and data state
in unit or Cypress tests. Cover the inverse action too; use Cypress when the
behavior requires a real browser primitive unavailable to the unit test
environment.
8. Use CSS Modules only: no Griffel, inline styles, or Tailwind.
9. Use variables from `stories/.storybook/tokens.css`; do not hardcode colors,
spacing, sizing, typography, radii, shadows, or motion values.
10. Style state through documented `data-*` selectors and semantic ARIA/native
selectors.
11. Preserve native inputs and semantic DOM. Visual indicators may be layered
around them, but must not replace them.
12. Use logical properties and verify RTL-sensitive layout; avoid physical
left/right properties unless the behavior is intentionally physical.
13. If the story adds transitions or animation, use motion tokens and provide a
`prefers-reduced-motion` override.
14. Preserve visible focus and state affordances in forced-colors mode when the
component needs custom borders, indicators, or overlays.
15. Verify focus-visible, disabled-focusable, light theme, and dark theme.
Before finishing story CSS, search the changed modules for raw color functions
and un-tokenized pixel/rem values. Reuse an existing token; if a component-local
calculation is unavoidable, derive a named CSS custom property from tokens
instead of repeating a magic value.
Stories are visual consumers, not part of the headless behavior implementation.
Never move story styles or design defaults into `library/`.
Build both Storybooks, open the new story in a browser, verify the primary
interaction and focus behavior, switch light/dark themes, and confirm the custom
"Show code" panel displays both the TSX and every referenced CSS Module.
## Documentation
Update the component's Storybook description with:
- purpose and composition model
- required parent/child relationships
- accessibility naming requirements
- stable `data-*` attributes and their values
- any important semantic-slot exception
If the package-level attribute contract changes, update both:
- `library/docs/Spec.md`
- `docs/react-v9/contributing/rfcs/react-components/convergence/headless-components.md`
Do not edit an accepted RFC merely to document one component that follows the
existing contract.
## Three-PR Workflow With `gh stack`
Use `gh stack` so reviewers see package-layer changes separately while the
headless PR can build against the component base APIs below it.
### Stack Shape
```text
master
└── <prefix>/<component>-test
└── <prefix>/<component>-base
└── <prefix>/<component>-headless
```
- **`<component>-test` PR (base: `master`)** — only regression tests that lock
down existing behavior; no production code and no Beachball change file.
在 GitHub 查看