# Fluent UI Insights - Theming *~2200 words* ## Welcome Welcome to another episode of Fluent UI Insights where we describe the architecture and decisions behind the Fluent UI design system. <!-- In previous episodes, we explored the different approaches to styling React components. In v0 and v8 we used CSSinJS with runtime computations and dynamic injection of CSS into the DOM. With the introduction of Griffel in v9, we have seen significant improvements in performance through build-time style computations and CSS extraction which has eliminated dynamic CSS injection. In this episode, we will discuss how theming fits into this picture. Join us as we continue to uncover the details of Fluent UI. --> From the previous episodes, you already know how CSS-in-JS solved performance issues for us. But there is still an uncovered concept related to styles. Join us as we discuss theming and how it works in Fluent UI. ## What is theming? Before we start, let me quickly explain what we mean by theming. Theming refers to the ability to switch between different appearances of an application, such as changing from light to dark mode, modifying a brand color ramp, or configuring spacing among elements for better user experience. Users can switch themes through the application preferences or at the operating system level. Theming can also be controlled by the application. It can affect the whole UI or just a smaller part of the application - we call that scoped theming. ## What is in the v0 theme In Fluent UI v0, the theme is composed of three main parts: - **Site variables**, where theme-wide tokens are defined. - **Component variables**, which are applicable to a single component. These should map to site variables but may also contain hardcoded values. - Finally, **component styles** which define the CSS for each component. The styles take component variables as input, but they also have access to the site variables. And, actually, to anything else. All these can be used not only as CSS style values but also as conditional code. *screenshot of a style function using component variables, site variables and conditions* This architecture allows for a lot of flexibility - the themes are completely independent of each other, so they can define different variables and styles to achieve any desired look. However, there are also some drawbacks to this approach: - The entire theme, including styles for all components, is represented by a single Javascript object. This object cannot be tree-shaken, so regardless of how small part of Fluent UI v0 an application uses, the entire theme will remain in the application bundle. - Theme merging is a complex task. For example, consider an application that uses scoped theming for a part of the UI. As described earlier, component variables depend on site variables, and component styles depend on both component and site variables. That means that component variables and styles must be implemented as functions. To merge themes, we need to merge site variables first, then component variables, and only after that can we merge the component styles. Merging in this context means invoking a chain of functions and merging their outputs. *screenshot from https://fluentsite.z22.web.core.windows.net/debugging#how-merging-works, or just a link?* - Theme merging is a performance-intensive task, that happens with every component render. We have introduced multiple optimizations and caching techniques to reduce the performance cost as much as possible, but it still comes at a cost. - Another challenge is the debug-ability of styles. With multiple theme layers and complex merging producing atomic CSS classes, the result looks like an assembly that nobody is able to read or reason about. To address this issue, we introduced a custom debug tool to help engineers analyze the merging process. ## What is in the v8 theme In Fluent UI v8, the approach to theme is slightly different - default styles are part of the components, while the variables are in the global theme object. It is possible to add component styles to the theme object as well. Those would be evaluated and merged with default component styles on every component render to allow style overrides. ## What is in the v9 theme In Fluent UI v9, we learnt from the v0 theming issues and built on top of v8 approach by incorporating component styles as part of the components themselves. This means that we no longer have a single, giant, unshakeable object. The styles are always the same, regardless of the selected theme. This approach allows us to process the styles during build time and even extract them to a static CSS file. But wait a second, if the styles are always the same, how can we theme the application? The answer is simple: CSS custom properties, also known as CSS variables. These were not an option in Fluent UI v0 or v8, but in v9, as we no longer support IE 11, we are free to use them. https://caniuse.com/css-variables The theme in Fluent UI v9 is simply a list of tokens represented by CSS variables in the DOM. To switch themes, the CSS variables are replaced with new values, and the application's appearance changes without changing any other styles. Supporting scoped theming is as easy as applying the CSS variables to a DOM subtree. The only added complexity are React portals - as those are rendered out of the natural DOM order and therefore outside the scope-themed sub tree, we need to guarantee that whenever a portal is rendered the appropriate set of CSS variables is applied to its DOM. ## Theme shape There are two types of tokens in the Fluent theme: global and alias tokens. Global tokens represent a dictionary of available or valid values. For example, a `pink` color ramp consist of twelve slots which map to hex color values. These are the only twelve shades of pink available in the theme. The values of global tokens usually do not change among themes; for instance, `pink.shade40` is represented by the same hex value in both light and dark themes. On top of global tokens, there are alias tokens, which point to global tokens instead of hardcoding any values. While global tokens use arbitrary names like `pink.shade40`, alias tokens use semantic names like `pinkBorderActive`, which are easier to understand. The mapping of alias tokens differs among themes; for example, the `pinkBorderActive` alias token references `pink.primary` in the light theme but `pink.tint30` in the dark theme. This is how theme switching works. In the Fluent UI v0 theme, all tokens were available in styles. But it is obvious that using a global color token is hardly ever correct. How often would you expect that a color is exactly the same hex value in both light and dark themes, or even in high contrast? To eliminate these bugs, Fluent UI v9 intentionally omits global color tokens from the theme. In the initial prototypes of the theme, we used a deeply nested object to represent the theme in order to provide a good developer experience for engineers. However, our assumptions proved to be incorrect. Instead of navigating a nested structure, we found that using IntelliSense on a flat object was much more effective. Representing the entire theme as a single flat object also made theme merging much more performant. Fluent UI v9 implements Fluent Design. But across Microsoft there are another libraries for other platforms which implement the same Fluent Design. It would not be scalable to keep all the implementations isolated. Updating a single token value would require implementing a change in multiple repositories in different programming languages. As a solution, we use a token pipeline inspired by style dictionary. Designers maintain a JSON file which holds the single source of truth for all the theme tokens. They use the tokens from the JSON file in their design tools. Token pipeline transforms the JSON files into source code for all the platforms. https://microsoft.github.io/fluentui-token-pipeline/ ## Performance challenges Whenever we've been measuring performance in Fluent UI v0 we solely focused on Javacript. All the computational work happened in Javascript so we could ignore all the other tasks in event loop. By introducing build time style processing, static CSS extraction and utilizing CSS variables, we offload more computations to the platform. Therefore we need to zoom out and consider the other tasks in the event loop as well. The Fluent UI v9 theme consisted of approximately 650 global tokens and 550 alias tokens. Adding hundreds of variables to DOM is not a big deal in JavaScript be we learnt that having those CSS variables present in DOM significantly affects style recalculation. In the first version, we injected all CSS variables for global tokens and in CSS variables for alias tokens, we referenced the CSS variables for global tokens: ```css --global-token: red; --alias-token: var(--global-token); ``` In a test environment with 6x CPU slowdown, we rendered 20 theme providers side by side, each inserting those 1200 CSS variables to DOM. After that Style recalculation takes up to 150ms. To optimize the performance, instead of referencing the global token CSS variables in alias tokens, we inlined the values to the alias tokens directly: ```css --global-token: red; --alias-token: red; /* inlined from global-token */ ``` The inlining improved style recalculation performance by 40%. As described earlier, we do not want the global tokens to be used directly. With that, we can remove them, going down from 1200 tokens to 550: ```css --alias-token: red; /* inlined from global-token */ ``` With that the time spent in style recalculation decreased by another 40%. Actually, based on additional measurements, we believe the style recalculation time linearly grows with the number of CSS variables. https://github.com/microsoft/fluentui/blob/master/rfcs/react-components/convergence/theme-shape.md The token names in the theme are quite long. By hashing them we also tested if this somehow affects performance but it seems that CSS variable name length has no impact. ## Are tokens flexible enough? Let's summarize the differences in theming between v0 and v9: - In v0, it is possible to have different component styles in different themes. In v9, the styles are always the same. - In v0, styles are produced by a function evaluated at runtime, which allows using v0 variables in conditions. However, V9 tokens can only be used as CSS values. Is this approach flexible enough to satisfy all theming needs? Before choosing this approach, we validated it in v0. Even though the themes in v0 can use different styles, we were able to implement dark and high contrast themes on top of a light theme by just overriding variables. Therefore, we believe this approach should be sufficient. But what if an application needs to use completely different styles? That is also possible by re-composing a component. All Fluent UI v9 components are authored in the form of hooks and a render function: ```tsx const Button = props => { const state = useButton(props); // processes props, generates state useButtonStyles(state); // generates styles, writes them to state return renderButton(state); // produces component JSX based on state } ``` Any application can recompose the individual hooks and either replace or augment the styles hook to achieve its desired look. To use the custom theme, the application needs to be built using the re-composed components. However, this approach would not work if the application needs to switch between a standard theme and the custom theme. It also disallows code reuse between an application built on standard Fluent UI components and one that uses the re-composed components. There are also use cases where applications use standard Fluent UI v9 components but want to tweak a particular design aspect across the entire application. Let's take the example of customizing button corner radius. This was possible in both v0 and v8 but cannot be achieved in v9. Styles are static and cannot be overridden at the application level. There is no support for component variables as adding hundreds of CSS variables would have a negative impact on performance. Even if there were component variables, you could only tweak aspects for which we expose tokens. Any other tweak would require a new component token to be added. Supporting application level style overrides or component variables would significantly increase the component's API surface. It would be challenging or even impossible to guarantee that the component would render as expected with all the possible combinations of props, component variables, and style overrides. Moreover, as Fluent UI v9 implements Fluent Design, overrides could cause the application to deviate from Fluent Design. These, along with performance concerns, are the main reasons why we have not supported such customizations. However, since this requirement is frequently requested, we are exploring various options for application-wide overrides: - Allowing style overrides though a React context. - Introducing component tokens by using CSS variables together with fallback values. - Using webpack aliases when bundling the application. https://github.com/microsoft/fluentui/pull/25333/files If you are interested you can check the linked RFC. And if you have any innovative ideas that we have not yet considered, please share them in the comments. ## High contrast I've mentioned high contrast support couple of times in this video but never explained that. There are two different ways how Fluent UI v9 supports high contrast - besides light a dark theme, there is a high contrast theme which maps all the alias color tokens to manually curated high contrast colors. The preferred approach on modern Windows is to use forced colors which follow operating system settings. We will dive deeper into this topic in an upcoming episode about accessibility. --- This is all for this episode, thanks for watching and stay tuned!