apps/public-docsite-v9/src/Concepts/AdvancedStylingTechniques.mdx
import { Meta, Source } from '@storybook/addon-docs/blocks';
<Meta title="Concepts/Developer/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:
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:
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 for a detailed explanation.
For example, an App.tsx might look like:
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 scaffolding is required to get here first. Define a useFancyButtonStyles.ts, and calculate the style similar to how you would for any other Fluent component.
⚠️ Custom style hooks must also append the slot's original className prop as returned by
getSlotClassNameProp_unstable, after the custom styles. This ensures that the className prop added by the user will take precedence over custom styles. See microsoft/fluentui#34166 for a detailed explanation.
import { getSlotClassNameProp_unstable, 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,
getSlotClassNameProp_unstable(buttonState.root),
);
if (buttonState.icon) {
buttonState.icon.className = mergeClasses(
buttonState.icon.className,
styles.icon,
getSlotClassNameProp_unstable(buttonState.icon),
);
}
};
Define the value for CustomStyleHooksProvider_unstable in FancyAppCustomStyleHooksValue.ts that consumes custom style hooks:
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:
// App.tsx
+ import { FANCY_CUSTOM_STYLE_HOOKS } from './FancyAppCustomStyleHooksValue';
<FluentProvider theme={webLightTheme}>
+ <CustomStyleHooksProvider_unstable value={FANCY_CUSTOM_STYLE_HOOKS}>
+ </CustomStyleHooksProvider_unstable>
</FluentProvider>
One caveat is that CustomStyleHooksProvider does not automatically merge contexts' values. In an app with 'Smart' styles for example:
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,
}}
>
</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:
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:
export function App() {
return (
<FluentProvider theme={webLightTheme}>
<CustomStyleHooksProvider_unstable
value={SMANCY_CUSTOM_STYLE_HOOKS}
>
</CustomStyleHooksProvider_unstable>
</CustomStyleHooksProvider_unstable>
</FluentProvider>
);
}
This way, apps can adopt the styles they need at scale while maintaining their own style preferences.