-
Notifications
You must be signed in to change notification settings - Fork 2.9k
docs(public-docsite-v9): add documentation for base state hooks and authoring patterns #35891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dmytrokirpa
wants to merge
2
commits into
microsoft:master
Choose a base branch
from
dmytrokirpa:docs/public-docsite-9/base-hooks
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+187
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
apps/public-docsite-v9/src/Concepts/BuildingCustomControls.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| import { Meta } from '@storybook/addon-docs/blocks'; | ||
|
|
||
| <Meta title="Concepts/Developer/Building Custom Controls" /> | ||
|
|
||
| ## Building custom controls | ||
|
|
||
| Base state hooks provide a low-level API for teams building custom component libraries with their own design systems. | ||
|
|
||
| They let you reuse Fluent UI behavior primitives (ARIA patterns, keyboard handling, semantic structure) while owning rendering and styling completely. | ||
|
|
||
| For Fluent UI v9 apps, the styled component APIs are the standard default. For Fluent DS extensions, existing component composition hooks are the right starting point. | ||
|
|
||
| ### Quick decision guide | ||
|
|
||
| Use existing composition hooks (`useButton_unstable`, `useInput_unstable`, etc.) when you want: | ||
|
|
||
| - Default styling with design tokens | ||
| - Simpler customization through style overrides | ||
| - Built-in accessibility and behavior | ||
|
|
||
| Use base state hooks (`useButtonBase_unstable`, `useInputBase_unstable`, etc.) when you need: | ||
|
|
||
| - Full control over rendering and styling architecture | ||
| - A custom design system with completely different visual patterns | ||
| - Fluent behavior and accessibility primitives without styling opinions | ||
|
|
||
| ### What base state hooks include | ||
|
|
||
| - Component behavior and state logic | ||
| - ARIA attributes and keyboard interaction patterns | ||
| - Semantic slot structure | ||
|
|
||
| ### What base state hooks exclude | ||
|
|
||
| - Design props (for example appearance, size, shape) | ||
| - Style logic (Griffel styles, design token styling) | ||
| - Motion and transitions | ||
| - Default slot implementations | ||
|
|
||
| ### Layer model | ||
|
|
||
| Components are composed in layers: | ||
|
|
||
| 1. `use{Component}Base_unstable` for behavior and structure | ||
| 2. `use{Component}_unstable` for component-level design concerns | ||
| 3. `{Component}` for the default styled experience | ||
|
|
||
| For example: | ||
|
|
||
| ```tsx | ||
| import { useButtonBase_unstable } from '@fluentui/react-button'; | ||
| import type { ButtonBaseProps } from '@fluentui/react-button'; | ||
|
|
||
| type MyButtonProps = ButtonBaseProps & { | ||
| variant?: 'primary' | 'secondary'; | ||
| }; | ||
| ``` | ||
|
|
||
| ### Authoring cookbook | ||
|
|
||
| The example below shows common authoring patterns when building custom components on top of base state hooks. | ||
|
|
||
| #### Full example: replacing slot logic and conditionally omitting slot rendering | ||
|
|
||
| This example shows the full flow (types, state, styles, render, component) for a loading-aware button that replaces icon/content rendering while loading. | ||
|
|
||
| ```tsx | ||
| import * as React from 'react'; | ||
| import { assertSlots, mergeClasses, slot, type Slot } from '@fluentui/react-components'; | ||
| import { useButtonBase_unstable } from '@fluentui/react-button'; | ||
| import type { ButtonBaseProps, ButtonBaseState } from '@fluentui/react-button'; | ||
|
|
||
| type LoadingButtonSlots = { | ||
| root: NonNullable<ButtonBaseState['root']>; | ||
| icon?: ButtonBaseState['icon']; | ||
| loadingIndicator?: Slot<'span'>; | ||
| }; | ||
|
|
||
| type LoadingButtonProps = ButtonBaseProps & { | ||
| isLoading?: boolean; | ||
| loadingIndicator?: Slot<'span'>; | ||
| }; | ||
|
|
||
| type LoadingButtonState = ButtonBaseState & { | ||
| isLoading: boolean; | ||
| loadingIndicator?: ReturnType<typeof slot.optional>; | ||
| components: LoadingButtonSlots; | ||
| }; | ||
|
|
||
| const useLoadingButtonState = ( | ||
| props: LoadingButtonProps, | ||
| ref: React.Ref<HTMLButtonElement | HTMLAnchorElement>, | ||
| ): LoadingButtonState => { | ||
| const { isLoading = false, loadingIndicator, ...baseProps } = props; | ||
| const baseState = useButtonBase_unstable(baseProps, ref); | ||
|
|
||
| return { | ||
| ...baseState, | ||
| isLoading, | ||
| loadingIndicator: slot.optional(loadingIndicator, { | ||
| renderByDefault: true, | ||
| defaultProps: { | ||
| children: 'Loading...', | ||
| 'aria-live': 'polite', | ||
| }, | ||
| elementType: 'span', | ||
| }), | ||
| components: { | ||
| ...baseState.components, | ||
| loadingIndicator: 'span', | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
| const useLoadingButtonStyles = (state: LoadingButtonState): void => { | ||
| state.root.className = mergeClasses('loadingButton', state.isLoading && 'loadingButton--busy', state.root.className); | ||
|
|
||
| if (state.loadingIndicator) { | ||
| state.loadingIndicator.className = mergeClasses('loadingButton__indicator', state.loadingIndicator.className); | ||
| } | ||
| }; | ||
|
|
||
| const renderLoadingButton = (state: LoadingButtonState) => { | ||
| assertSlots<LoadingButtonSlots>(state); | ||
|
|
||
| return ( | ||
| <state.root> | ||
| {state.isLoading ? ( | ||
| // Replaces icon/content rendering while loading | ||
| state.loadingIndicator && <state.loadingIndicator /> | ||
| ) : ( | ||
| <> | ||
| {state.icon && <state.icon />} | ||
| {state.root.children} | ||
| </> | ||
| )} | ||
| </state.root> | ||
| ); | ||
| }; | ||
|
|
||
| export const LoadingButton = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, LoadingButtonProps>( | ||
| (props, ref) => { | ||
| const state = useLoadingButtonState(props, ref); | ||
| useLoadingButtonStyles(state); | ||
| return renderLoadingButton(state); | ||
| }, | ||
| ); | ||
| ``` | ||
|
|
||
| When replacing slot logic, verify keyboard behavior, ARIA semantics, and focus handling still match component expectations. | ||
|
|
||
| ### Authoring checklist | ||
|
|
||
| - Preserve base root props and ref wiring from `use{Component}Base_unstable`. | ||
| - Keep user `className` precedence by appending existing slot class names last. | ||
| - Validate all states visually (`:hover`, `:active`, `:focus-visible`, disabled). | ||
| - Test keyboard and screen reader behavior after custom rendering changes. | ||
| - Prefer small wrapper functions (e.g., `useCustomButtonState`, `useCustomButtonStyles`, `renderCustomButton`) for maintainability. | ||
|
|
||
| ### Accessibility responsibilities | ||
|
|
||
| Base state hooks provide ARIA and interaction behavior, but they do not provide visual accessibility defaults. | ||
|
|
||
| When you implement custom styles, ensure: | ||
|
|
||
| - Visible focus indicators | ||
| - Sufficient color contrast | ||
| - Clear hover, pressed, and disabled visual states | ||
|
|
||
| For accessibility guidance, see [Accessibility](?path=/docs/concepts-developer-accessibility-components-overview--docs). | ||
|
|
||
| ### Relationship to other customization options | ||
|
|
||
| - Use [Styling Components](?path=/docs/concepts-developer-styling-components--docs) for standard style overrides. | ||
| - Use [Advanced Styling Techniques](?path=/docs/concepts-developer-advanced-styling-techniques--docs) for app-wide style hook customization. | ||
| - Use [Customizing Components with Slots](?path=/docs/concepts-developer-customizing-components-with-slots--docs) for part-level composition within existing components. | ||
|
|
||
| ### Stability and API lifecycle | ||
|
|
||
| Base state hooks use the `_unstable` suffix following Fluent UI's stability pattern. The `_unstable` prefix indicates APIs that may evolve as we refine the component architecture patterns and best practices. | ||
|
|
||
| For deeper rationale and rollout context, see the RFC: [Component Base State Hooks](https://github.com/microsoft/fluentui/blob/master/docs/react-v9/contributing/rfcs/react-components/convergence/building-custom-controls.md). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.