Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Meta, Source } from '@storybook/addon-docs/blocks';

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.

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

Most of the Fluent UI React v9 components are structured using the hooks approach:

```tsx
Expand Down
182 changes: 182 additions & 0 deletions apps/public-docsite-v9/src/Concepts/BuildingCustomControls.mdx
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).
1 change: 1 addition & 0 deletions apps/public-docsite-v9/src/Concepts/Slots/Slots.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Later in this topic, you will find examples covering each of these scenarios.
- If you want to change how a component behaves, make significant layout and style changes,
replace non-slot parts, or wrap a component with different props, then consider using the hooks API.
The hooks API gives you complete control to recompose a component but is more complex than using slots.
See [Building custom components](?path=/docs/concepts-developer-building-custom-controls--docs) for guidance.

### Conditional rendering

Expand Down
2 changes: 2 additions & 0 deletions apps/public-docsite-v9/src/Concepts/StylingComponents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Meta, Source } from '@storybook/addon-docs/blocks';

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.

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

### Getting started

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.
Expand Down
Loading