Blazor offers a wide range of solutions with application-wide CSS, CSS Isolation and leveraging CSS variables. Choosing the right architecture for styling components requires knowing how the components will be used by developers.
This article is part of the C# Advent calendar. During the event, two new articles are posted each day by talented authors throughout the community.
Choosing the architecture for styling a Blazor application will directly affect the ability to customize the theme of a Razor component, or even the entire application. Deciding to use a global CSS file to apply a theme, or using component CSS Isolation will directly impact the scope of customization which can be applied to elements in the application. In this article we’ll discuss the varying levels of CSS scope in a Blazor application and explore opportunities to expand or restrict access to theme changes.
Let’s begin by defining what CSS scope is in this context—not to be mistaken for CSS Scoped Styles. We’ll be discussing the scope of CSS properties and how they relate to Razor components and the access level to CSS properties within Razor components.
There is currently a CSS spec which defines “CSS Scoped Styles.” CSS Scoped Styles introduce a syntax to CSS which enable developers to write scoped selectors using an
@
syntax similar to CSS media queries.
Generally a property in CSS is globally accessible because whenever a statement using the same selector appears afterward it will override the previous value. In the following example the color
property is assigned the value red
,
then the second statement reassigns the color
property to blue.
.selector {
/* property: value; */
color: red;
}
.selector {
color: blue; /* blue replaces red */
}
The result is similar to what we might expect from a public
class property in C# code. Because the property is public, it can be arbitrarily reassigned by a developer by setting the property.
public class Foo
{
public string Color { get;set; }
}
fooInstance.Color = "red";
fooInstance.Color = "blue";
// fooInstance.Color value is "blue"
In this example, color
is essentially on the global scope; there is no way to control access to color
or when and how the property is assigned. If we need to control access, we’ll need to apply some architectural patterns
to do so. Through features of Blazor and CSS, we can control the scope of style using CSS Isolation, but we need to understand the impact our decisions have first.
In Blazor, CSS Isolation is a compiler feature that isolates CSS styles to individual page, view or components. Components using CSS Isolation can reduce or avoid dependencies on global styles.
CSS Isolation is also intended to assist with application maintenance in two ways. Isolated CSS code is associated with a specific component through unique selectors which remove the CSS from the global scope. In addition, all CSS generated by the compiler
from CSS Isolation is output to a file named {Namespace}.style.css
. By auto generating the CSS at compile time, we get a fresh copy on the CSS; if a component is deleted, the CSS is also removed from the compilation.
Style conflicts in CSS Isolation are managed by creating CSS selectors with a very high specificity, making them impossible to override by mistake. The effect is essentially an anti-pattern, taking the "Cascading" out of Cascading Style Sheets
(CSS).
In the following example a component named Bubble is created. Bubble.razor is the markup for the component, while Bubble.razor.css is the isolated CSS belonging to it. Together they create a round shape with a blue background and white text. The compiler will alter the HTML and CSS by adding a unique id in the form of an HTML attribute for each element in the component. The unique id is also incorporated into the selectors of the CSS so that the id must be used to style the associated HTML.
@* Bubble.razor *@
<div>
<span class="inner">@Text</span>
</div>
/* Bubble.razor.css */
.inner {
font-size: 16px;
background-color: blue;
color: white;
/* properties make it round */
line-height: 3em;
height: 3em;
width: 3em;
border-radius: 50%;
display: block;
text-align: center;
margin: .5em;
}
The code from above is altered by the compiler to produce the example in below as it would be rendered by the browser. The unique identifier b-qfafx2ebd2
ensures that the selector is not accidentally used to overwrite any of the element’s
properties.
@* rendered Bubble HTML *@
<div b-qfafx2ebd2>
<span b-qfafx2ebd2 class="inner">@Text</span>
</div>
/* rendered Bubble.razor.css */
.inner[b-qfafx2ebd2] {
font-size: 16px;
background-color: blue;
color: white;
/* properties make it round */
line-height: 3em;
height: 3em;
width: 3em;
border-radius: 50%;
display: block;
text-align: center;
margin: .5em;
}
Try the interactive Blazor REPL below. In this example one component uses a global scope, while the second uses CSS Isolation. In the Main.razor component a snippet of CSS overwrites the color value which has no effect on the isolated component.
Using CSS Isolation takes scope from global to completely private. To change a value of a property used in isolated CSS, the source code must be updated. If we try to equate this to C# code, it would resemble a const
.
public class Foo {
const string color = "red";
}
This approach creates a very rigid but safe design where nothing can influence the style of the component. However, the source code must be edited to make changes and the application will need to be recompiled.
With Blazor there are essentially two architecture patterns that exist by default—standard CSS implemented in the global scope or the completely private scope of CSS Isolation. The type of Razor Components you’re developing and their desired usage will ultimately decide what approach works best. The decision should reflect what type of customization your end users need.
For some scenarios, global customization might be ideal. One such example would be a generic component library with many consumers. Organizations using a component library will likely implement their own design system and theme the components to match. Teleik UI for Blazor is one such library that offers this sort of customization.
On the opposite end of the spectrum, an organization developing an internal application or line of business app would benefit from CSS Isolation. A single design system is implemented and enforced through CSS Isolation. While rigid, it reduces the ability for developers to circumvent the design system, especially by accident.
These polar opposite architectures can also be blended to create another option. This option would allow a sliding scale of scope so an organization can define where customization is allowed and how broadly it is applied using global, local and private scopes.
Global Customization:
Balanced Customization:
Minimal Customization:
Customization features can be implemented through techniques using component parameters and CSS custom properties. Together we can control access much like C# can: with private and public modifiers. Finding the right balance on the scale will depend on the requirements, but the techniques provide flexibility.
Allowing customization of components through parameters is fairly common when creating Razor components. Parameters create an API for developers to make changes in a controlled manner. Parameters can be loosely defined strings or use more restrictive types like bools, enums or classes. When parameters are used to theme a component, it’s often to give developers a choice between predetermined variations of the component’s theme.
Let’s begin with an example of restricting a components theme to simple variations using an enum
. For this example we’ll create a button component with five theme options: Default, Info, Warning, Danger and Success. These themes
provide developers with options via parameter, but also will restrict the customization to the included theme variations using CSS Isolation.
First, we’ll create a Button component and define the enum values.
<button @onclick="@HandleClick">@Text</button>
@code {
//.. simplified for example purposes
public enum ButtonTheme {
Default,
Danger,
Warning,
Info,
Success
}
}
Next, we’ll add a Theme
parameter and modify the button markup to write out a class
attribute based on the parameter’s value. The value will append the text button
to the lowercase value of the enum selected.
<button class="button @Theme.ToString().ToLowerInvariant()" ...
@code {
//.. simplified for example purposes
[Parameter]
public ButtonTheme Theme { get; set; } = ButtonTheme.Default;
}
Then a .razor.css file is created using CSS Isolation, Button.razor.css
. In the CSS file we’ll write the corresponding CSS for the various button themes. The selectors will include button
with the appended theme selection,
ex:
button.{ButtonTheme}
.
.button {
/* default && base button style */
color: #1d1942;
background-color: transparent;
border: 2px solid #1d1942;
/*... shortened for simplicity*/
border-radius: 5px;
}
.button:hover {
color: white;
background-color: #1d1942;
}
.danger {
color: #e55039;
background-color: transparent;
border: 2px solid #e55039;
}
.danger:hover {
background-color: #e55039;
}
/*... shortened for simplicity*/
The complete component offers a few theme options strictly defined by the API using enums. Using the Button component is straightforward since the only acceptable values come from ButtonThemes.
<Button Text="Default"/>
<Button Text="Info" Theme="@Button.ButtonTheme.Info"/>
<Button Text="Danger" Theme="@Button.ButtonTheme.Danger"/>
<Button Text="Warning" Theme="@Button.ButtonTheme.Warning"/>
<Button Text="Success" Theme="@Button.ButtonTheme.Success"/>
This pattern offers some flexibility and is easily understood by the developer using the component. In addition, all of the CSS code is protected by CSS Isolation, which means no other button variations can be made and no accidental CSS collisions can occur.
Try the interactive Blazor REPL below. Take note of the different theme options and how they are applied.
Using parameters we opened our component up to modification. With this approach we’re still required to edit the source code for any theme changes. If there was a need to use this component with a new design system or a major re-branding, it would take a considerable amount of work to update all of the component’s individual CSS Isolation files. Fortunately we can expand our scope to be completely flexible while remaining prescriptive by leveraging CSS custom properties.
Adding CSS custom properties to your architecture will vastly improve the extensiblity of your your CSS code. Custom properties (also referred to as CSS variables) are entities defined in CSS that contain specific values to be reused throughout a document.
To use variables a name is declared using the --*
syntax, then the var()
function is used to retrieve the value.
/* set */
--primary-color: blue;
/* gets blue from --primary-color */
color: var(--primary-color);
When using the var
function to get a value, a second parameter can be passed as default to be used in case the variable is invalid. Default values help us write reliable code and provide a scheme to make some values optional.
/* --my-var not declared */
color: var(--my-var, red);
/* result -> red */
/* set --my-var */
--my-var: blue;
color: var(--my-var, red);
/* result -> blue */
Variables are scoped to the element they are declared on, and participate in the cascade. In addition, variables inherit their values from their parent. Together these rules form scope within the stylesheet and can be leveraged to create additional scope within components.
Try the interactive Blazor REPL below. Take note of how inheritance applies the value on each element.
Using CSS variables and CSS Isolation together, we can define scope in several ways, getting the best of both global and private access. With this pattern we’ll use CSS variables and their scoping mechanics to define global scope, then use default values to create private scope, and control access with component parameters.
First we’ll need to set up the global variables for our application. These variables will be defined in a standard app.css file. The global variables need to be defined in a :root
selector and include things like our application theme
colors.
:root {
font-size: 32px;
--app-background: antiquewhite;
--primary-color: #aeeaf8;
--primary-color-darker: #1d1942;
--primary-contrast-color: var(--app-background);
--secondary-color: #fd6012;
--secondary-color-darker: #ed451f;
--secondary-contrast-color: var(--app-background);
}
Next we’ll create a component, for simplicity we’ll reuse the previous Bubble component which renders a circle with text inside.
<div>
<span>@Text</span>
</div>
@code {
[Parameter]
public string Text { get; set; } = "CSS";
}
Then we’ll use CSS Isolation by creating a .razor.css file for the Bubble component. Inside the file we’ll define the basic style of the span element.
span {
font-size: 16px;
background-color: blue;
color: white;
line-height: 3em;
height: 3em;
width: 3em;
border-radius: 50%;
display: block;
text-align: center;
margin: .5em;
}
Now it’s time to define access to the component’s style. Let’s start by giving developers an API for changing the components colors. Right now the color
and background-color
properties are hard-coded to white
and blue. Let’s update these with CSS variables that are privately scoped. To make this effective, we’ll rely on inheritance by adding a class to the outer container of the component. For this example, the class is named private-scope
.
<div class="private-scope">
<span>@Text</span>
</div>
A new CSS selector .private-scope
is added and inside CSS variables are defined. Because of CSS isolation, these variables will be private to the component. Here we can control how the theme is applied by using default values. A variable
named --private-bubble-bg-color
is added and uses a currently undefined variable --bubble-bg-color
as its color. A default is used to get the global color from the application theme --primary-color
. This setup
enables the Bubble to receive color from the application’s theme, or if a developer defines --bubble-bg-color
then all Bubble component instance will receive that color instead.
--private-bubble-bg-color: var(--bubble-bg-color, var(--primary-color));
--private-bubblecolor: var(--bubble-text-color, var(--primary-color-contrast));
}
To enable these new values, we’ll wire up the privately scoped values by updating the span
selector.
.private-scope { ... }
span {
background-color: var(--private-bubble-bg-color);
color: var(--private-bubble-color);
If we would like to use the public setting to theme all Bubble component instances, we can update the :root
selector in app.css. In the example below, the Bubbles are now themed using secondary colors instead of primary.
:root {
font-size: 32px;
--app-background: antiquewhite;
--primary-color: #aeeaf8;
--primary-color-darker: #1d1942;
--primary-contrast-color: var(--app-background);
--secondary-color: #fd6012;
--secondary-color-darker: #ed451f;
--secondary-contrast-color: var(--app-background);
/* set the Bubble color theme globally */
--bubble-bg-color: var(--secondary-color-darker);
--bubble-text-color: var(--secondary-contrast-color);
}
Try the interactive Blazor REPL below. Modify the values for --bubble-*
or try commenting them out to see how the theme changes.
Currently our Bubble component uses a global and private scope. Let’s expand upon the example by adding public scope. By adding parameters to our Bubble component, we can allow individual customization per component instance.
We’ll start by adding properties to our Bubble component for setting the background color and text color. Since these parameters will be CSS color values we’ll use the string
value type.
[Parameter]
public string? BackgroundColor { get; set; }
[Parameter]
public string? TextColor { get; set; }
Next, we need to translate the properties into an inline style. While it’s uncommon to see inline styles used in modern HTML/CSS, in this case we’re leveraging their specificity to override the variable values at the element level. This ensures we override CSS isolation as well as any inherited values.
The desired output from our code will be an inline style with the format --private-bubble-bg-color: value; --private-bubble-color: value
. To simplify the process, we’ll use a NuGet package called BlazorComponentUtilities.
BlazorComponentUtilities’s StyleBuilder defines an inline style with a fluent API.
The following code creates an inline style while eliminating the properties if they are null.
string inlineStyle => StyleBuilder.Empty()
.AddStyle(prop: "--private-bubble-bg-color",
value: BackgroundColor,
when: !string.IsNullOrWhiteSpace(BackgroundColor))
.AddStyle(prop: "--private-bubble-color",
value: Color,
when: !string.IsNullOrWhiteSpace(Color))
.NullIfEmpty()
;
Finally we’ll update the markup with the inline style generated by StyleBuilder to a style
attribute.
<div class="private-scope" style="@inlineStyle">
<span>@Text</span>
</div>
Now we can set the color values per component instance.
<Bubble BackgroundColor="blue" TextColor="yellow"/>
Try the interactive Blazor REPL below. Modify the values for --bubble-*
or try commenting them out to see how the theme changes.
If we compare the CSS code for this pattern to C# some similarities emerge. The global CSS variables loosely translate to a constructor assignment, while private CSS variables work much like a private field, and properties expose the backing fields.
public class Bubble
{
private string _bgColor;
private string _textColor;
public Bubble(
string textColor = "red",
string backgroundColor = "white")
{
_textColor = textColor;
_bgColor = backgroundColor
}
public string BackgroundColor {
get { return _bgColor; }
set { _bgColor = value; }
}
public string TextColor {
get { return _textColor; }
set { _textColor = value; }
}
We learned how to create a range of customization features in our own components using CSS and CSS Isolation. Many of these customization features can be found in the Telerik UI for Blazor component library. Telerik UI for Blazor incorporates Sass and CSS to globally style components to match your company brand or design system. The component library can be customized through Sass, CSS or with a WYSIWYG online ThemeBuilder.
In addition to global styles, components can be themed at the instance level by many built-in style options as component parameters. Any built-in style option can be defined by string or using helpers provided by the API such as "ThemeConstants.Button.Rounded.Medium". The API helps with consistency and discoverability, and prevents typos.
The parameters support preset theme options, or can be completely customized. Telerik UI for Blazor handles custom styles at the parameter level by building a CSS selector based on the string used in the style property.
For example, setting the component’s Rounded property to: “small,” “medium,” “large” or “full” will use a built-in style. These styles are added to the component as a class value of k-rounded-{value}
.
We can also use the class to create a custom style by using a string that is not one of the built-in styles. If Rounded is set to the string “custom
” it will produce the class k-rounded-custom
, then we’ll
write a selector in our style sheet to create the custom effect.
.k-rounded-custom {
width:200px;
height:200px;
border-radius:50%;
}
Try the interactive Blazor REPL below. In this demo a TelerikButton component is data-bound to various parameters showing some of the preset theme options as well as a custom option.
Blazor offers a wide range of solutions with application wide-CSS, CSS Isolation and leveraging CSS variables. Choosing the right architecture for styling components requires knowing how the components will be used by developers. The right balance of customization will be different for each application. With the techniques learned here, you’ll be able find the right fit.
Ed