Skip to content

Commit 0e111eb

Browse files
authored
docs(public-docsite-v9): add documentation for base state hooks and authoring patterns (#35891)
1 parent 8244b43 commit 0e111eb

File tree

4 files changed

+213
-0
lines changed

4 files changed

+213
-0
lines changed

apps/public-docsite-v9/src/Concepts/AdvancedStylingTechniques.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { Meta, Source } from '@storybook/addon-docs/blocks';
66

77
Theming, applying style changes at scale, is a really important part of any design system. Teams should look to tokens and variables first when considering how to change the look and feel of an app. Sometimes more powerful tools are required to accomplish a goal or handle edge cases. This document explains how to leverage the custom style hooks built into Fluent UI React V9.
88

9+
> This page focuses on style customization of existing Fluent components. If you need architecture-level control over rendering and want Fluent behavior primitives without default styling, see [Building custom components](?path=/docs/concepts-developer-building-custom-controls--docs).
10+
911
Most of the Fluent UI React v9 components are structured using the hooks approach:
1012

1113
```tsx
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { Meta } from '@storybook/addon-docs/blocks';
2+
3+
<Meta title="Concepts/Developer/Building Custom Controls" />
4+
5+
## Building custom controls
6+
7+
A custom control is a component you own entirely — its markup, its styles, its design tokens — that still needs to behave like its Fluent counterpart: correct ARIA roles, keyboard navigation, focus management, and slot structure.
8+
9+
Building one from scratch means reimplementing behavior that Fluent UI has already solved. Base state hooks let you avoid that. They expose the behavior layer of a Fluent component as a standalone API you can call directly, while leaving rendering and styling entirely up to you.
10+
11+
### When to use base state hooks
12+
13+
There are three levels of customization available, depending on how much control you need:
14+
15+
**Styled components** (`Button`, `Input`, etc.) — the default for Fluent UI v9 apps. Use when you want built-in accessibility, design tokens, and standard visual behavior with optional style overrides.
16+
17+
**Composition hooks** (`useButton_unstable`, `useInput_unstable`, etc.) — the right choice for Fluent DS extensions. Use when you need to own rendering but still want Fluent's visual design language as a starting point.
18+
19+
**Base state hooks** (`useButtonBase_unstable`, `useInputBase_unstable`, etc.) — use when you need:
20+
21+
- Full control over rendering and styling architecture
22+
- A custom design system with completely different visual patterns
23+
- Fluent behavior and accessibility primitives without any styling opinions
24+
25+
Base state hooks are not a replacement for composition hooks in most cases. If you are customizing a Fluent component's appearance or adding design variants, composition hooks are the right starting point.
26+
27+
### What base state hooks include
28+
29+
- Component behavior and state logic
30+
- ARIA attributes and keyboard interaction patterns
31+
- Semantic slot structure (which slots exist and their expected element types)
32+
33+
### What base state hooks exclude
34+
35+
- Design props (for example `appearance`, `size`, `shape`)
36+
- Style logic (Griffel styles, design token styling)
37+
- Motion and transitions
38+
- Default slot content
39+
40+
### Layer model
41+
42+
Components are composed in three layers. Each layer builds on the one below it:
43+
44+
1. `use{Component}Base_unstable` — behavior, state, and semantic structure. No styling.
45+
2. `use{Component}_unstable` — applies design-level concerns (appearance, size) on top of base state.
46+
3. `{Component}` — the default styled experience, combining state, styles, and rendering.
47+
48+
When building a custom control, you consume layer 1 directly and replace layers 2 and 3 with your own state hook, styles hook, and render function.
49+
50+
```tsx
51+
// Layer 1: consume base behavior directly
52+
import { useButtonBase_unstable } from '@fluentui/react-button';
53+
import type { ButtonBaseProps, ButtonBaseState } from '@fluentui/react-button';
54+
55+
// Add your custom props on top of the base props type
56+
type MyButtonProps = ButtonBaseProps & {
57+
variant?: 'primary' | 'secondary';
58+
};
59+
60+
// Your state hook wraps useButtonBase_unstable and extends the returned state
61+
const useMyButtonState = (props: MyButtonProps, ref: React.Ref<HTMLButtonElement>) => {
62+
const { variant, ...baseProps } = props;
63+
const baseState = useButtonBase_unstable(baseProps, ref);
64+
return { ...baseState, variant };
65+
};
66+
```
67+
68+
The full example in the next section shows layers 2 and 3 (styles and rendering) as well.
69+
70+
### Authoring cookbook
71+
72+
The example below shows the complete pattern for building a custom component on top of a base state hook. It uses a loading-aware button that replaces icon and content rendering while loading.
73+
74+
#### Full example: custom state, styles, and conditional slot rendering
75+
76+
```tsx
77+
import * as React from 'react';
78+
import { assertSlots, mergeClasses, slot, type Slot } from '@fluentui/react-components';
79+
import { useButtonBase_unstable } from '@fluentui/react-button';
80+
import type { ButtonBaseProps, ButtonBaseState } from '@fluentui/react-button';
81+
82+
// --- Types ---
83+
// Extend base slot and state types with your custom additions.
84+
85+
type LoadingButtonSlots = {
86+
root: NonNullable<ButtonBaseState['root']>;
87+
icon?: ButtonBaseState['icon'];
88+
loadingIndicator?: Slot<'span'>;
89+
};
90+
91+
type LoadingButtonProps = ButtonBaseProps & {
92+
isLoading?: boolean;
93+
loadingIndicator?: Slot<'span'>;
94+
};
95+
96+
type LoadingButtonState = ButtonBaseState & {
97+
isLoading: boolean;
98+
loadingIndicator?: ReturnType<typeof slot.optional>;
99+
components: LoadingButtonSlots;
100+
};
101+
102+
// --- State hook ---
103+
// Delegate ARIA, keyboard, and semantic behavior to the base hook.
104+
// Add your component-specific state on top.
105+
106+
const useLoadingButtonState = (
107+
props: LoadingButtonProps,
108+
ref: React.Ref<HTMLButtonElement | HTMLAnchorElement>,
109+
): LoadingButtonState => {
110+
const { isLoading = false, loadingIndicator, ...baseProps } = props;
111+
const baseState = useButtonBase_unstable(baseProps, ref);
112+
113+
return {
114+
...baseState,
115+
isLoading,
116+
// slot.optional resolves the slot prop: uses the caller's value if provided,
117+
// otherwise renders the defaultProps content. Returns undefined if renderByDefault is false.
118+
loadingIndicator: slot.optional(loadingIndicator, {
119+
renderByDefault: true,
120+
defaultProps: {
121+
children: 'Loading...',
122+
'aria-live': 'polite',
123+
},
124+
elementType: 'span',
125+
}),
126+
components: {
127+
...baseState.components,
128+
loadingIndicator: 'span',
129+
},
130+
};
131+
};
132+
133+
// --- Styles hook ---
134+
// Apply class names using mergeClasses. Always append the existing slot className last
135+
// so that consumer class names take precedence over your internal ones.
136+
137+
const useLoadingButtonStyles = (state: LoadingButtonState): void => {
138+
state.root.className = mergeClasses(
139+
'loadingButton',
140+
state.isLoading && 'loadingButton--busy',
141+
state.root.className, // consumer class names win
142+
);
143+
144+
if (state.loadingIndicator) {
145+
state.loadingIndicator.className = mergeClasses('loadingButton__indicator', state.loadingIndicator.className);
146+
}
147+
};
148+
149+
// --- Render function ---
150+
// Controls what gets rendered and when. Use assertSlots to get type-safe slot
151+
// components (state.root, state.icon, etc.) and render them as JSX elements.
152+
153+
const renderLoadingButton = (state: LoadingButtonState) => {
154+
assertSlots<LoadingButtonSlots>(state);
155+
156+
return (
157+
<state.root>
158+
{state.isLoading ? (
159+
state.loadingIndicator && <state.loadingIndicator />
160+
) : (
161+
<>
162+
{state.icon && <state.icon />}
163+
{state.root.children}
164+
</>
165+
)}
166+
</state.root>
167+
);
168+
};
169+
170+
// --- Component ---
171+
172+
export const LoadingButton = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, LoadingButtonProps>(
173+
(props, ref) => {
174+
const state = useLoadingButtonState(props, ref);
175+
useLoadingButtonStyles(state);
176+
return renderLoadingButton(state);
177+
},
178+
);
179+
```
180+
181+
### Authoring checklist
182+
183+
- Preserve base root props and ref wiring — always pass the ref to `use{Component}Base_unstable`, never attach it yourself.
184+
- Keep user `className` precedence by passing existing slot class names as the last argument to `mergeClasses`.
185+
- After any rendering changes, verify keyboard behavior, ARIA semantics, and focus handling still work as expected.
186+
- Validate all visual states (`:hover`, `:active`, `:focus-visible`, disabled).
187+
- Test with a screen reader after custom rendering changes.
188+
- Keep the state, styles, and render logic in separate functions (`useCustomButtonState`, `useCustomButtonStyles`, `renderCustomButton`) for maintainability.
189+
190+
### Accessibility responsibilities
191+
192+
Base state hooks provide ARIA attributes and interaction patterns, but they do not enforce visual accessibility.
193+
194+
When implementing custom styles, ensure:
195+
196+
- Visible focus indicators on all interactive elements
197+
- Sufficient color contrast ratios
198+
- Distinct hover, pressed, and disabled visual states
199+
200+
For detailed accessibility guidance, see [Accessibility](?path=/docs/concepts-developer-accessibility-components-overview--docs).
201+
202+
### Relationship to other customization options
203+
204+
- Use [Styling Components](?path=/docs/concepts-developer-styling-components--docs) for standard style overrides.
205+
- Use [Advanced Styling Techniques](?path=/docs/concepts-developer-advanced-styling-techniques--docs) for app-wide style hook customization.
206+
- Use [Customizing Components with Slots](?path=/docs/concepts-developer-customizing-components-with-slots--docs) for part-level composition within existing components.
207+
208+
For deeper rationale and design decisions, see the RFC: [Component Base State Hooks](https://github.com/microsoft/fluentui/blob/master/docs/react-v9/contributing/rfcs/react-components/convergence/base-state-hooks.md).

apps/public-docsite-v9/src/Concepts/Slots/Slots.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Later in this topic, you will find examples covering each of these scenarios.
4848
- If you want to change how a component behaves, make significant layout and style changes,
4949
replace non-slot parts, or wrap a component with different props, then consider using the hooks API.
5050
The hooks API gives you complete control to recompose a component but is more complex than using slots.
51+
See [Building custom components](?path=/docs/concepts-developer-building-custom-controls--docs) for guidance.
5152

5253
### Conditional rendering
5354

apps/public-docsite-v9/src/Concepts/StylingComponents.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { Meta, Source } from '@storybook/addon-docs/blocks';
66

77
Visit the **[Styling handbook](https://github.com/microsoft/fluentui/blob/master/docs/react-v9/contributing/rfcs/react-components/styles-handbook.md)** for a comprehensive styling guide, as this article only introduces basics to get you started quickly.
88

9+
> If you need to build a custom component architecture (not just style existing components), see [Building custom components](?path=/docs/concepts-developer-building-custom-controls--docs).
10+
911
### Getting started
1012

1113
To style Fluent UI React v9 components `makeStyles` is used. `makeStyles` comes from [Griffel](https://griffel.js.org) a homegrown CSS-in-JS implementation which generates atomic CSS classes.

0 commit comments

Comments
 (0)