Lately, I’ve been helping a few developers solve some of their issues around Blazor adoption. While I have mixed feelings about the technology, my urge to help developers overcome their challenges outweighs my trepidation with the technology itself.

Recently, I had a Blazor developer have issues with the ::deep cascading style sheet (CSS) pseudo-selector and the style’s inability to cascade styles to child components. If you’re having a similar issue, I have a detailed issue breakdown and potential solution. Let’s get started.

What is CSS isolation?

Blazor is a component-based framework that takes much inspiration from the JavaScript ecosystem’s React component model. The model focuses on reusability through encapsulation and manifests in a single-file style that combines Razor markup and C# logic.

In a Blazor application, CSS can be scoped to a single component by adding a convention-based filename similar to a component. For example, if your application contains a Counter.razor component, any styles defined in Counter.razor.css would be scoped to the component definition.

Blazor scopes CSS by adding a component-unique HTML attribute to the HTML rendered on the client. Take, for example, the contents of Counter.razor.css, which I’ve added to a new Blazor application.

h1 {
    color: pink;
}

The resulting CSS is sent to the client when the Blazor build pipeline processes the file.

/* /Pages/Counter.razor.rz.scp.css */
h1[b-13zswy4nzk] {
    color: pink;
}

This CSS rule reads, “any H1 in the current document with an attribute of ‘b-13zswy4nzk’ should have pink-colored text.”

Scoped CSS allows you to target components with styles without the risk of having rules conflict with other rules. But what if you want to take advantage of the cascading part of CSS?

Using the ::deep pseudo-selector

As you saw, Blazor processes component-scoped CSS files and adds them to your application’s Blazor CSS file. This allows Blazor developers to use additional pseudo-selectors to generate different rules. One such pseudo-selector is the ::deep selector.

The ::deep pseudo-selector will allow you to target child HTML elements nested within a parent component. Let’s change the previous CSS rule to use this feature.

::deep h1 {
	color: pink;
}

After running the Blazor application, we can see the updated CSS rule.

/* /Pages/Counter.razor.rz.scp.css */
[b-13zswy4nzk] h1 {
    color: pink;
}

This new CSS rule reads, “any H1 element found within an element with the attribute of ‘b-13zswy4nzk’ will have a text color of pink.”.

Great! Ship it. What’s the issue?

The Blazor attribute problem

Let’s take a look at the Counter.razor component and a new Child.razor component with the previous isolated CSS in mind. First, the Counter.razor definition.

@page "/counter"

<PageTitle>Counter</PageTitle>
<Child/>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

You’ll note the usage of the Child component right under the PageTitle component. Let’s see that implementation.

<h1>Counter</h1>

@code {
    
}

Given the isolated CSS, we would assume that the H1 element within our child component would become pink, but that’s not the case. Let’s look at the rendered HTML to see the issue.

<article class="content px-4" b-5dhe1ruqj7>
    <h1 tabindex="-1">Counter</h1>
    <p role="status" b-13zswy4nzk>Current count: 0</p>
    <button class="btn btn-primary" b-13zswy4nzk>Click me</button>
</article>

Do you see the issue? Blazor adds the unique attribute to all top-level HTML elements within a component. This creates a mismatch between markup and the CSS rule.

In the rendered HTML, the p and button elements get the attribute b-13zswy4nzk, but our Child component is used within the Counter component but before the any HTML element. Worse, there are no encapsulating HTML elements.

The fix to ::deep styles

So you’ve read this far and want to know the solution to the problem. It’s relatively straightforward.

If your usage of the ::deep pseudo-selector is not working, you need to add a wrapping HTML element within your parent component that is a parent to all usages of child components.

Let’s fix our Counter.razor markup with a straightforward change.

@page "/counter"

<div>
	<PageTitle>Counter</PageTitle>
	<Child/>
	<p role="status">Current count: @currentCount</p>
	<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</div>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

When we re-run our application, we can see that the unique attribute is now on the first HTML element along with the previous elements of p and button and our CSS rule now targets nested HTML elements correctly.

<div b-13zswy4nzk>
    <h1 tabindex="-1">Counter</h1>
    <p role="status" b-13zswy4nzk>Current count: 0</p>
    <button class="btn btn-primary" b-13zswy4nzk>Click me</button>
</div>

This fixes the issue, but depending on the CSS framework library you’re using, a parent div HTML element may break your layout. Experiment with different HTML tags to see which ones you can use as wrappers while not breaking global CSS rules.

Conclusion

CSS Isolation is a technique for continuing the philosophy of encapsulating as much within the bounds of a component, but there are times where you want the cascade to do its job. To get the ::deep pseudo-selector element working properly, all your Blazor components should have a single-wrapping HTML element as the root element. If your Blazor components do not have a parent element, you’ll get CSS rules that don’t ultimately match your HTML markup.

Hope this post helped, and as always, thanks for reading and sharing my posts with friends and colleagues. Cheers.