技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
headless-component【数据与研究】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.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Author $ARGUMENTS in:
packages/react-components/react-headless-components-preview/librarypackages/react-components/react-headless-components-preview/storiesHeadless 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.
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:
packages/react-components/react-<name>/library/src/library/src/components/Button/library/src/components/Accordion/library/src/components/Menu/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.mdpackages/react-components/react-headless-components-preview/stories/README.mddocs/react-v9/contributing/rfcs/react-components/convergence/headless-components.mdThe 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.
Before writing code:
as behavioruse<Name>Base_unstable, render<Name>_unstable,
use<Name>ContextValues_unstable, context hooks, and shared utilities.data-* contract before implementation. Include only
behavior/base state, never visual design state.types/exports -> hook/context -> rendered DOM/slots -> test or story.data-*
contract, headless tests, stories, public subpath, headless bundle-size
coverage, API docs, manifest updates, and change fileDo 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.
Every public headless primitive exposes stable, unsuffixed names:
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:
useComponentBase_unstable
↓
useComponent
↓
renderComponent
↓
Component
The headless layer must not add styling, design props, motion, default icons, or new interaction behavior.
For a simple component:
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:
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.
'use client'; to component and hook modules that use React client APIs.ForwardRefComponent<Props> with React.forwardRef for ref-capable
primitives. Never use React.FC.JSXElement; do not invent a fake ref and do not use
React.FC.displayName on every exported React component.bundleIcon at module scope, never inside a hook or render function.'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.
useComponentNameBase_unstable hook.useComponentNameBase_unstable(props, ref?), with an explicitly typed
props parameter and ref typed as React.Ref<...>.useComponentName_unstable hook. Run the component-package lint target so
base-hook-signature and base-hook-no-forbidden-runtime validate the split.useFluent_unstable() for targetDocument and its defaultView; never
access window, document, or navigator directly.data-* AttributesState attributes are the supported CSS targeting contract.
'' when true and omit when false.
Reserve explicit string values for enum or tri-state contracts.ComponentState so it appears in generated API docs.Use toDataAttributeValue from library/src/utils for presence and string
attributes:
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.
data-*
attributes.string.any or unnecessary type assertions to hide an API mismatch.Re-export or alias the upstream unstable renderer when its slot structure is already correct.
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.
When the headless primitive needs new base APIs from the corresponding
@fluentui/react-* component package, update all applicable surfaces in the
middle PR:
ComponentBaseProps, ComponentBaseState, and the existing slots type.useComponentBase_unstable from the component implementation barrel.library/src/index.ts.generate-api and commit the updated library/etc/*.api.md files.library/package.json when runtime imports or
public entry points change:
dependenciespackage.json rather than inventing a rangetypes, node, import, and
require entries and ensure its built files are included by filespackage.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.
Update every required surface in the top PR:
library/src/components/ComponentName/index.tslibrary/src/component-name.tslibrary/package.json:
@fluentui/react-* package to dependencies if it
is not already presentexports["./component-name"] with matching types, node, import,
and require paths"./package.json" retained in the package's established positionexports["./package.json"]files entries still publish the generated
declaration and JavaScript files; update files only if the new artifact
falls outside those patternssideEffects: false only while the new modules remain free of
import-time side effectsgenerate-api and commit the new or updated library/etc/component-name.api.md
plus any aggregate API report changed by the export.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 coverage is required in both PR layers and must be reviewable in the PR that introduces the relevant public API.
packages/react-components/react-<name>/library/bundle-size/.<ComponentName>.fixture.js that imports the component from the package root,
references it (normally with console.log), and exports a stable fixture
name.bundle-size target and inspect the generated
report for unexpected growth.Update
library/bundle-size/AllComponents.fixture.js for every new public component
subpath:
import * as ComponentName from '@fluentui/react-headless-components-preview/component-name';.ComponentName to the logged object.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:
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.
Add focused tests beside every public primitive.
isConformant for every ref-capable componentclassName, style, as, and slot forwarding where applicabledata-* attribute:
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 demonstrate that the unstyled primitive can support an independent design system. Styling belongs only in the stories package.
ComponentNameDescription.md.index.stories.tsx.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.subcomponents for compound APIs.stories/.storybook/tokens.css; do not hardcode colors,
spacing, sizing, typography, radii, shadows, or motion values.data-* selectors and semantic ARIA/native
selectors.prefers-reduced-motion override.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.
Update the component's Storybook description with:
data-* attributes and their valuesIf the package-level attribute contract changes, update both:
library/docs/Spec.mddocs/react-v9/contributing/rfcs/react-components/convergence/headless-components.mdDo not edit an accepted RFC merely to document one component that follows the existing contract.
gh stackUse gh stack so reviewers see package-layer changes separately while the
headless PR can build against the component base APIs below it.
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.<component>-base PR (base: <component>-test) — only the corresponding
@fluentui/react-* component package, its tests/API/bundle fixture, its
manifest/lockfile changes, and its Beachball change file.<component>-headless PR (base: <component>-base) — only
react-headless-components-preview library/stories/docsite-facing changes,
its manifest/lockfile changes, its bundle fixture, generated API docs, and its
Beachball change file.If no component-package code change is required because every needed base API is already published, do not create empty test or base PRs. Use one PR for the headless change and state that the upstream export inventory was verified. If a base extraction does not need new regression coverage, omit only the test PR and use a two-PR base/headless stack.
Select <stack-remote> before initializing: the Git remote whose GitHub
repository should host the PRs. Do not assume origin is correct. In a fork
checkout, origin often points to the fork while review belongs in
microsoft/fluentui. gh stack submit creates PRs in the selected remote's
repository and has no separate --repo/base-repository flag.
Verify the selected remote URL and push permission before initializing the stack. If the intended repository is upstream but the contributor can push only to a fork, surface that limitation rather than silently creating PRs in the fork; use the repository's approved cross-fork PR workflow instead.
Drive gh stack non-interactively: always pass explicit branch names, use
gh stack submit --auto and gh stack view --json, and pass
--remote <stack-remote> to submit/push/sync because this repository normally
has more than one remote configured.
Each publishable layer generates its own change file against the trunk with an
explicit --package, so an inherited down-stack change never leaks into the
wrong file:
# middle branch, after editing only the component package and its tests
yarn beachball change --no-commit --branch master \
--package @fluentui/react-<name> --type minor \
--message "feat: expose headless base APIs"
# top branch, after editing only the headless library and stories
yarn beachball change --no-commit --branch master \
--package @fluentui/react-headless-components-preview --type patch \
--message "feat: add <ComponentName>"
Commit each change file with the files it describes, plus yarn.lock only when
Yarn updated it.
The test-only bottom PR does not receive a Beachball change file because it does not alter published output.
yarn change or the non-interactive
yarn beachball change form; never create or edit them manually.minor
changes. The headless preview package follows zero-semver, so additive
headless primitives and exports use patch until that package is stable.
Use patch for corrections without new public API as well, and never use
major without explicit approval.Run tasks through Nx and use the narrowest checks that cover the change:
# bottom test-only PR
yarn nx run react-<name>:test
# middle component-package PR
yarn nx run react-<name>:test
yarn nx run react-<name>:lint
yarn nx run react-<name>:type-check
yarn nx run react-<name>:build
yarn nx run react-<name>:generate-api
yarn nx run react-<name>:bundle-size
# top PR
yarn nx run react-headless-components-preview:test --runTestsByPath \
packages/react-components/react-headless-components-preview/library/src/components/ComponentName/ComponentName.test.tsx
yarn nx run react-headless-components-preview:lint
yarn nx run react-headless-components-preview:type-check
yarn nx run react-headless-components-preview:build
yarn nx run react-headless-components-preview:generate-api
yarn nx run react-headless-components-preview:bundle-size
yarn nx run react-headless-components-preview:e2e
yarn nx run react-headless-components-preview:test-rit
yarn nx run react-headless-components-preview-stories:lint
yarn nx run react-headless-components-preview-stories:type-check
yarn nx run react-headless-components-preview-stories:build-storybook
yarn nx run react-headless-components-preview-stories:test-ssr
yarn nx run public-docsite-v9-headless:build-storybook
Run e2e when adding or changing browser-dependent interaction tests. Run
cross-version React and SSR targets when the component, renderer, portal, or
story composition changes. Use yarn nx show project <project> to confirm
target names before invoking a target not listed above.
Inspect generated API files and confirm the new stable subpath, hook, renderer,
types, context exports, and data-* attributes are present.
data-* attribute.AllComponents.fixture.js.