Skip to content

Commit

Permalink
Pattern doc on custom style hooks (#33740)
Browse files Browse the repository at this point in the history
  • Loading branch information
mltejera authored Feb 5, 2025
1 parent d0b0f4f commit a826c3b
Showing 1 changed file with 174 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { Meta, Source } from '@storybook/addon-docs';

<Meta title="Concepts/Developer/Advanced Styling Techniques" />

## Advanced styling techniques

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.

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

```tsx
export const Button: ForwardRefComponent<ButtonProps> = React.forwardRef((props, ref) => {
const state = useButton_unstable(props, ref);

useButtonStyles_unstable(state);
useCustomStyleHook_unstable('useButtonStyles_unstable')(state);

return renderButton_unstable(state);
}) as ForwardRefComponent<ButtonProps>;
```

The pertinent lines are where styles are calculated:

```ts
useButtonStyles_unstable(state);
useCustomStyleHook_unstable('useButtonStyles_unstable')(state);
```

The default styles are defined by `useButtonStyles_unstable` and are packaged with every component. `useCustomStyleHook_unstable` reaches into a `CustomStyleHooksProvider` (if it exists) and calculates any styles that match the component type.

> 💡See RFC [microsoft/fluentui#25333](https://github.com/microsoft/fluentui/pull/25333) for a detailed explanation.
For example, an `App.tsx` might look like:

```tsx
import { Button, FluentProvider, webLightTheme, CustomStyleHooksProvider_unstable } from '@fluentui/react-components';
import { AlertRegular } from '@fluentui/react-icons';
import { FANCY_CUSTOM_STYLE_HOOKS } from './FancyAppCustomStyleHooksValue.ts';

export function App() {
return (
<FluentProvider theme={webLightTheme}>
<Button>I am a Vanilla Fluent Button</Button>
<CustomStyleHooksProvider_unstable value={FANCY_CUSTOM_STYLE_HOOKS}>
<Button icon={<AlertRegular />}>I am a *Fancy* Button</Button>
</CustomStyleHooksProvider_unstable>
</FluentProvider>
);
}
```

A little scaffodling is required to get here first. Define a `useFancyButtonStyles.ts`, and calculate the style similar to how you would for any other Fluent component:

```ts
import { makeStyles, type ButtonState } from '@fluentui/react-components';

const useStyles = makeStyles({
root: {
// These are all unique to Fancy theme.
border: '2px solid green',
backgroundColor: 'pink',
borderRadius: '64px',
},
icon: {
color: 'blue',
backgroundColor: 'white',
},
});

export const useFancyButtonStyles = (state: unknown) => {
const buttonState = state as ButtonState;
const styles = useStyles();
buttonState.root.className = mergeClasses(buttonState.root.className, styles.root);

if (buttonState.icon) {
buttonState.icon.className = mergeClasses(buttonState.icon.className, styles.icon);
}
};
```

Define the value for `CustomStyleHooksProvider_unstable` in `FancyAppCustomStyleHooksValue.ts` that consumes custom style hooks:

```ts
import { type CustomStyleHooksContextValue } from '@fluentui/react-components';
import { useFancyButtonStyles } from './useFancyButtonStyles';

export const FANCY_CUSTOM_STYLE_HOOKS: CustomStyleHooksContextValue = {
useButtonStyles_unstable: useFancyButtonStyles,
// ... more component styles as needed for your theme.
};
```

Finally use the `CustomStyleHooksProvider_unstable` in your app:

```diff
// App.tsx
+ import { FANCY_CUSTOM_STYLE_HOOKS } from './FancyAppCustomStyleHooksValue';

<FluentProvider theme={webLightTheme}>
+ <CustomStyleHooksProvider_unstable value={FANCY_CUSTOM_STYLE_HOOKS}>
{/* application code ... */}
+ </CustomStyleHooksProvider_unstable>
</FluentProvider>
```

### Nesting and merging custom style hooks

One caveat is that `CustomStyleHooksProvider` does not automatically merge contexts' values. In an app with 'Smart' styles for example:

```tsx
export function App() {
return (
<FluentProvider theme={webLightTheme}>
<CustomStyleHooksProvider_unstable
// Ideally these are abstracted to another file object,
// But are consolidated for demonstration brevity.
value={{
useButtonStyles_unstable: useSmartButtonStyles,
useImageStyles_unstable: useSmartImageStyles,
}}
>
<CustomStyleHooksProvider_unstable
value={{
useButtonStyles_unstable: useFancyButtonStyles,
}}
>
{/* ⚠️ The nested "CustomStyleHooksProvider_unstable" provider completely overwrites the Smart values. */}
{/* I.e. only "useFancyButtonStyles" will be passed down. */}
{/* The app will only look Fancy, but not Smart. It needs both.*/}

{/* application code ... */}
</CustomStyleHooksProvider_unstable>
</CustomStyleHooksProvider_unstable>
</FluentProvider>
);
}
```

Applications should make the best decisions for their styles, determining when and how to apply them. If apps have their own custom style hooks and want to adopt others, they can gracefully merge them:

```tsx
export const useSmancyCustomButtonStyles = (state: unknown) => {
const buttonState = state as ButtonState;

// "Fancy" comes first
useFancyButtonStyles(buttonState);
// "Smart" comes second, so it will win where there are conflicts
useSmartButtonStyles(buttonState);
};

export const SMANCY_CUSTOM_STYLE_HOOKS: CustomStyleHooksContextValue = {
useButtonStyles_unstable: useAppCustomButtonStyles,
// ... more component style overrides
};
```

And finally the App code can be simplified:

```tsx
export function App() {
return (
<FluentProvider theme={webLightTheme}>
<CustomStyleHooksProvider_unstable
value={SMANCY_CUSTOM_STYLE_HOOKS}
>
{/* application code ... */}
</CustomStyleHooksProvider_unstable>
</CustomStyleHooksProvider_unstable>
</FluentProvider>
);
}
```

This way, apps can adopt the styles they need at scale while maintaining their own style preferences.

0 comments on commit a826c3b

Please sign in to comment.