Hello!
From the previous episodes,
by the way the link is somewhere here.
You already know how CSS-in-JS
solved performance issues for us.
But there is still an uncovered concept
related to styling.
This episode is about theming.
As always deep dive into architecture decisions
and performance problems.
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 theme
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.
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.
And finally,
"Component Styles"
which define the CSS for each component.
The styles take "component variables" as an 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.
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 as for 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 styles" and "component variables"
must be implemented as functions.
And 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.
Theming merging
is a performance-intensive task.
And 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
custom debug tool to help engineers
analyze the merging process.
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 same object as well.
Those would be evaluated and merged
with default component styles on every
component render to allow style overrides.
In Fluent UI v9 we learned
from the v0 theming issue
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 giant, single, unshakable object.
It also means that 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.
The theme in Fluent UI v9 is simply
a list of tokens represented by
CSS variables in the DOM.
To switch theme, the CSS variables
are replaced with new values
and the application's appearance changes
without changing any other styles.
Supporting scope theming is also 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 of the scope theme subtree.
We need to guarantee that
whenever a Portal is rendered
the appropriate set of CSS variables
is applied to its DOM.
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
consists of 12 slots
which map to hex color values.
These are the only 12 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 are 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 both in light and dark theme,
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
emerging 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 file
into source code for all the platforms.
Whenever we have been measuring performance
in Fluent UI v0,
we solely focused on JavaScript.
All the computational work happened in JavaScript
so we could ignore all the
other tasks in browser 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 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.
But we learned 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.
In a test environment
with 6x times 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 150 ms.
To optimize the performance
instead of referencing
the global token CSS variables
in alias tokens,
we inline the values
to the alias tokens directly.
The inlining improves
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.
With that, the time spent in style recalculation
decreased by another under 40%.
Actually, based on additional measurements
we believe the style recalculation time
linearly grows with
the number of CSS variables.
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 lenght has no impact.
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 and 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?
This is also possible by
re-composing a component.
All Fluent UI v9 components are authored
in the form of hooks and a render function.
Any application can recompose the individual hooks
and either replace or augment
the styles hooks to achieve its desired look.
To use the custom theme,
the application needs to be built
using the recomposed components.
However, this approach would not work
if the application needs to
switch between a standard theme
and a custom theme.
It also disallows code reuse
between an application built on
standard Fluent UI components and one
that uses the recomposed 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's corner radius.
This was possible in both v0 and v8,
but cannot be achieved in v9.
Styles are static and cannot be
overridden at 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
through a React context,
introducing component tokens
by using CSS variables
together with fallback values
or using Webpack aliases
when building the application.
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 comments.
I have 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 and 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!
Oh, God! The answer is simple!
It's worse,
when you are trying to be engaging.
It starts to be creepy!
[laughing]
I am not an engaging person,
I'm serious or looking serious,
so that's what I should stick to.