docs/react-v9/contributing/rfcs/react-components/convergence/theme-tokens.md
@miroslavstastny
This RFC describes types of tokens available in a Theme, proposes theme override possibilities and introduces on-demand component tokens.
Fluent UI Components are themed using a Theme injected by FluentProvider. The Theme is an object which contains tokens - named variables. Instead of passing theme tokens directly to components, FluentProvider injects them as CSS variables into DOM, components then reference the CSS variables in their makeStyles calls:
// Theme object
const theme = {
global: {
color: {
black: '#000' // token value defined for global.color.black
}
}
}
// Application root - creates a div, injects --global-color-black: #000 to its styles
<FluentProvider theme={theme}>
<ThemedComponent />
</FluentProvider>
// Fluent UI Themed Component
const useStyles = makeStyles([[
null,
theme => ({ // typed object which matches the theme object structure...
color: theme.global.color.black // ...but value is a css variable reference: 'var(--global-color-black)'
})
]]
const ThemedComponent = () => {
const cls = useStyles({})
return <div className={cls}>ThemedComponent</div>
}
Currently, there are two different kinds of tokens - global and alias tokens.
Global tokens represent named raw values (constants). One name represents the same value no matter whether it is light, dark or high contrast theme.
const theme = {
global: {
color: {
black: '#000', // global.color.black will always be #000
},
},
};
Currently, global tokens are the same across all the themes with brand color ramp being the only exception.
Alias tokens do not represent direct raw values, but are rather mapped to global tokens. The same alias token is mapped to different global tokens in different themes.
const themeLight = {
alias: {
color: {
brandForeground: brand.shade10,
},
},
};
const themeDark = {
alias: {
color: {
brandForeground: brand.tint10, // different global token than in light
},
},
};
There is nothing like component tokens in the Theme. Components use alias or global tokens directly.
If an application needs to tweak look and feel of Fluent UI components, it can change the theme for the whole application:
const tweakedTheme = mergeThemes(teamsLightTheme, {
global: {
borderRadius: {
medium: '6px', // change medium border radius to 6px for the whole application
},
},
});
This change will affect all instances of all Fluent UI components which use medium border radius in the whole application.
If the application needs to tweak only its one part (screen), it will wrap that part with additional provider overriding necessary variables:
const CustomScreen = () => {
// overrides only, FluentProvider will merge with a theme from context
const themeOverrides = {
global: {
borderRadius: {
medium: '6px',
},
},
};
return (
<FluentProvider theme={themeOverrides}>
<CustomScreenInner />
</FluentProvider>
);
};
To override tokens for a single instance of a component, you can wrap the component with a ThemeProvider.
However, we have always considered wrapping a single component with its own ThemeProvider an antipattern.
As the ThemeProvider consumes a React context, merges themes and creates a new context, its usage is not free!
const radiusOverrides = {
global: {
borderRadius: {
medium: '6px',
},
},
};
<ThemeProvider theme={{ radiusOverrides }}>
<Button>I have custom border radius</Button>
</ThemeProvider>;
You can also override styles using makeStyles:
const useRadiusOverrides = makeStyles([[null, { borderRadius: '6px' }]]);
const CustomButton = ({ className, ...rest }) => {
const overrides = useRadiusOverrides({});
const classes = ax(overrides, className);
return <Button className={classes} {...rest} />;
};
There is no way you could override border-radius for all Button instances in your application without altering other components. If you change global.borderRadius.medium, all components that use that token would be affected.
Button uses this token directly, as described earlier, there is not any component token like buttonBorderRadius which you could override.
If engineers see that the options above do not work for their use case, they should talk to design about adding a component token to a theme.
There are basically the two following rules:
componentRadiusMedium satisfies design needs, it is better token than buttonPrimaryTopLeftRadius.Be proactive and create all possible component tokens (like buttonPrimaryTopLeftRadius).
Pros:
Cons:
ComponentCustomizer componentCustom component which would create a wrapping DOM element and inject overrides as CSS variables without consuming or creating a new React context.
Pros:
Cons:
ThemeProviderReact.PortalPass the override tokens to a component. The component can then set the CSS variables on its root element.
Pros:
Cons:
React.PortalGlobal tokens are the same across Light-Dark-HC and differ between Teams vs Web only in Brand color. Should the global be part of the theme or a separate thing? Should brand be a part of global or not?
We are blaming the browser for its inability to handle thousands of CSS variables but have actually never measured the perf impact. We should get numbers!