|
| 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