Dark Mode

Nick Van Exan

Software Developer

Blazor: A Primer for UI Devs

Opinionated notes from the field

For the past few months I’ve been working on a client project that leverages Microsoft’s Blazor technology for building web applications. The following are some notes and thoughts I have about the technology, from the perspective of a UI developer previously focused on the JavaScript ecosystem.

What is Blazor?

Blazor is a framework for building interactive client-side web UI with .NET. You can liken it to React, though a better analogue might be Next.JS. It is much larger than a rendering library; it is a full framework, complete with its own opinions and technologies for routing, form handling, server-side rendering and much more. Like other modern front-end frameworks, Blazor envisions composing UI in a component-based architecture.

One of the primary differences between Blazor and other frameworks is that you write your UI in C#, a statically typed language used primarily for back-end development.

Blazor components are .NET C# classes built into .NET assemblies. The component class is usually written in the form of a Razor1 markup page with a .razor file extension (e.g. Button.razor2).

Here’s an example of a Blazor component defined in a Razor page:

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<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++;
    }
}

Blazor can run (and in my view is intended primarily to run) on the server.3 This makes Blazor more like Next.JS than simple client-side React. As with Next.JS, this changes the designs and patterns you may use for development. For example, you don’t need to call an API when you can query the database directly and pull back the data before the component is rendered.

Another important difference between Blazor and other frameworks is in how client-server communication happens. Updating UI content and handling events happen over a SignalR connection using the WebSockets protocol.4

That sets the table. Let’s move on to the meal. In the next sections I’m going to delve into some features of Blazor and the trade-offs I’ve encountered working with it in the field.

C# vs. JS / TS

There are important differences between C# and JavaScript. If you’re coming to Blazor from the JavaScript ecosystem, the differences that will be most relevant to you are:

Many JS devs are actually writing TypeScript these days. And so many of the differences in terms of static typing are increasingly negligible, as production grade JS is increasingly looking like C#. But whereas TypeScript allows for escape hatches to apply greater or lesser type safety as needed, C# does not.

Practically, this means you have great type safety at compile time. This makes the developer experience enjoyable in certain contexts. You certainly detect a lot more errors or breaking changes at compile time, which is useful particularly when refactoring. You can also fairly easily rename classes and namespaces, again useful when refactoring.

But you will also spend a lot more time taming OOP and learning idiosyncrasies of how C# works within the context of .NET.5

Component Ergonomics

Building components in Blazor is similar to building components in other frameworks. Components consist of razor templates for rendering HTML, C# classes for writing component logic, and css files for writing styles.

All three of these elements can be combined in a single razor page to author a component. Here’s an example:

@namespace App.Common.Callouts

<div class="@($"app-callout {Variant?.ToString().ToLower()}")">
    @Content
</div>

@code {
    [Parameter] public RenderFragment? Content { get; set; }
    [Parameter] public Variant? Variant { get; set; }

    public enum Variant
    {
        Error,
        Warning,
        Notice
    }
}

<style>
    .app-callout {
        border: solid 1px var(--app-colors-light-grey);
        background: var(--app-colors-white);
        padding: 1rem;
    }
	
    .app-callout.error {
        border-color: var(--app-colors-red);
        background: var(--app-colors-light-red);
    }
</style>

This is a simple component that renders a callout on a screen. It takes a parameter called Content of RenderFragment type and renders that within a containing div, that attaches styles to the containing callout box.

There are two things to notice here. The first is that we’re using specific syntax for C# and razor files to do the rendering. This is great if you’re a C# dev because you can use the syntax you’re already familiar with. If you're coming to Blazor without much C# experience, there is going to be yet-another syntax to learn to render things on a page. The upside, however, is what you are writing is C#. So as long as you’re down for learning C#, the razor templating syntax will fit in pretty well along your journey without getting in your way.

The second thing to notice is that while all the constituent elements of a component (template, logic, styles) can be written in a single .razor file, in practice they are commonly not, and instead are separated into three files:

The reason for this is because without a corresponding .razor.cs file, Visual Studio and Visual Studio Code struggle with IntelliSense and namespaces with razor files. Further, if you want your styles to be scoped to the component (i.e. css isolation), you need to put them in a separate .css file.

The key takeaway here is that if you want CSS isolation and you want your developer experience to work properly, you have to break your components into three separate files. Some developers prefer the separation of these three concerns into separate files, so I’ll let you decide how you feel about that. Personally, I like to work with all three concerns in the same file where it makes sense to do so, and so I tend to think of this as an area needing improvement.

The Developer Experience in VS Code

If you’re coming to Blazor from the JavaScript ecosystem, it is likely you are not using Visual Studio as your IDE, which is what professional C# developers use.

I can say as a Mac user and JavaScript / TypeScript developer that you definitely can write and build Blazor apps without Visual Studio. You can use the .NET Core CLI to run, debug, watch, test, and build your projects or solutions, and it works very well. You can then also use Visual Studio Code to write your app.6

With Visual Studio Code, there are certain extensions you’re going to need to write C# for Blazor apps. These include:

Having said that, I can also say that the developer experience using Visual Studio Code is not as good as using Visual Studio. And in some cases can be difficult to work with. For example, I have found that over time the C# extension can become slow and dysfunctional. The linter seems to struggle in particular with razor files, marking many things with phantom errors or warnings that simply aren’t there when you run a clean build. This is an area that has a lot of room for improvement.

Routing and Navigation

Routing in Blazor is quite simple and quite good. You simply add @page directives to a Blazor component, and boom, you have a route.

@page "/blazor-route"
@page "/different-blazor-route"

<PageTitle>Routing</PageTitle>

<h1>Routing Example</h1>

<p>
    This page is reached at either <code>/blazor-route</code> or 
    <code>/different-blazor-route</code>.
</p>

There’s support for parameters, query params, and so on, and it’s very easy to integrate.

There’s also great support out of the box for auth guarding. Want to protect a route? Just add an authorization attribute.

@attribute [Authorize(Roles = "Admin")]

JS Interop

One aspect of Blazor that UI devs may find amusing is a feature called JS interop. With JS interop, a Blazor app can invoke JavaScript functions from .NET methods and .NET methods from JS functions.

The JS interop feature is a concession that to be a web developer you still need to know and write JavaScript. It’s how we talk to the browser. It’s how we communicate with the DOM.

Similar to other Frameworks, Blazor assumes you won’t need to interact with the DOM directly and generally advises against doing so. But it also assumes you’ll need JS very minimally, and likely won’t be writing much yourself.

This second assumption results in a developer experience that is less than ideal. To be sure, you can import and reference third party JS modules and talk to them in your C# code. But the experience of doing so can be a bit clunky. First, you have to put your JS module somewhere to be served as a static asset. Then you have to use the JS interop to interact with it. Here's an example from the Microsoft docs for using a simple JS script to trigger a prompt.

@page "/call-js-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 6</PageTitle>

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

It works well enough, but is it elegant? I'm not so sure.

The same is true if you are writing your own JavaScript. For example, let’s say you’re asked to implement a tooltip component. You can achieve a basic level of functionality with just HTML/CSS but for more advanced edge cases you’re going to need to get into window viewport sizes and collision detection to do things like dynamically move a tooltip to the other side when the browser width moves below a certain breakpoint. You will likely need use of the window:matchMedia API and the Element:getBoundingClientRect API. And because the creators of Blazor don’t think you will or should be writing much JavaScript, there’s no helpful defaults when you scaffold a new application about using TypeScript instead of JS, having a test harness for your TS/JS related code, and so on.7 You have to scaffold that yourself. At which point, once you start scaffolding a new TS repo with Jest for unit testing, etc. you may start to wonder... what have I really gained here?

To be sure, a Blazor app is designed for you to not have to write too much JS. But it's important to realize that when you do, inevitably, have to write JS, it can be a bit cumbersome to set up and implement.

Ecosystem

Blazor is built on .NET and so as a developer you have access to whatever is available in that ecosystem. This can be positive and negative.

For things like security, or validation, or auto-mapping of objects, there’s libraries that exist in the .NET world that are awesome and a dream to use. And while you still need to import packages from a package manager, overall the developer experience is much better with NuGet than it is with NPM.

The trouble with the Blazor ecosystem, however, is that it is relatively limited when it comes to UI development compared to the dramatic growth of libraries and tooling in the JavaScript ecosystem over the last 10-15 years.

One important example is Storybook. Storybook is a frontend workshop for building UI components and pages in isolation. Thousands of teams use it for UI development, testing, and documentation. It’s open source and free. And it does not exist in the Blazor ecosystem.

Animated screenshot of Storybook
Animated screenshot of Storybook

Most C# devs I’ve talked to about this have no idea what Storybook is, and that’s understandable, as they historically have not built a lot of UI. But those who have spent time building out component libraries and design systems understand the value of this tool and related tooling.

Most devs understand that design system level components should be isolated and packable as a package for other teams to consume. And that works fine in Blazor. Just create a new razor class library and publish it on NuGet and you’re golden. But the next hurdle is... how do you document those components, and make them available for other teams to understand and play with? Further, how do you test those components for different scenarios, such as dark mode, accessibility, and so on? And what tooling are you putting in your CI pipeline to catch regressions for your components across their myriad states?

In the JavaScript ecosystem, Storybook and Chromatic are important tools that have made building and testing component libraries, and sharing their documentation and APIs, a lot easier, and so much so that many organizations have developed “Story-Driven Development” as a practice.8

Currently, if you are looking for a Storybook-like solution, the only example I could find is a project created by a developer and maintained by a couple of others called BlazingStory. It does not have feature parity with Storybook and the Storybook team has no plans to support Blazor at this time.

All of this could change over time, to be sure. But at the moment, if you’re coming to Blazor from the JavaScript ecosystem, you are going to find the lack of some of this tooling a bit jarring.

Testing

Unit testing capabilities in Blazor are fairly good and should be familiar to developers coming from the JavaScript ecosystem. In .NET you can choose your unit testing framework of your choice, and then you can add a library called bUnit to layer on top of that as your actual testing library for components.

Here’s a mapping of the unit testing technologies in the Blazor and React ecosystems:

TechnologyBlazorReact
Unit test runnerxUnit, NUnit, MSTestJest
Testing librarybUnitReact Testing Library
Snapshot testingVerifyJest snapshots
A11Y testingn/aJest-Axe

bUnit is a capable testing library and its format will be familiar to those with experience using React Testing Library. The aim in both cases is to test components as they are rendered, and how they would be used by a user.

using Xunit;
using Bunit;

public class HelloWorldTest : TestContext
{
  [Fact]
  public void HelloWorldComponentRendersCorrectly()
  {
    // Act
    var cut = RenderComponent<HelloWorld>();

    // Assert
    cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
  }
}

In practice, I have found bUnit has some limitations relative to React Testing Library with respect to simulating certain inputs and handling keyboard events. The bUnit ecosystem also lacks accessibility matchers, so whereas with Jest you can use the jest-axe library to assert that a component has no accessibility violations, you currently have to manually write tests for accessibility related functionality you want to test in your components.

Accessibility

With Blazor being based on .NET and C#, you may be wondering what its a11y story looks like. In general, I would say that Blazor itself as a technology does not prevent the development of accessible web applications, but it doesn't always make it easy either. You can use proper standard elements like <button>, you can add aria attributes to elements, you can add event handlers to control interactions on key presses, and you can set focus to elements. Some of this last mile work does require use of the JS interop, though, which can be a bit clunky to use.

An additional caution. If you’re looking to leverage an existing third-party component library for Blazor, you are going to find the options available at this time to be sorely lacking, particularly when it comes to accessibility. In my own review of the leading UI component libraries for Blazor back in October 2023, I found not one delivered WCAG AA compliant components. Many failed even basic keyboard or screen-reading implementation tests.

LibraryAccessible?Notes
MudBlazorNoRoles, aria attributes, keyboard and focus support all lacking
MatBlazorNoRoles, aria attributes, keyboard and focus support all lacking
AntBlazorNoRoles, aria attributes, keyboard and focus support all lacking
BlazoriseNoA bit better than others, but keyboard support isn't fully there (see Tabs), Date Picker is not accessible, etc.
RadzenNoa11y issues with many components (DatePicker, Dialog, Grid, Select / Dropdown, etc.)
Fluent-UINoA bit better than others, but still a number of issues (DatePicker for example)
TelerikClaims to beA bit better than the other libraries, but requires commercial license, and API doesn’t allow you to add your own aria attributes in many components (no attribute splatting permitted), making it difficult to correct defects without support tickets.

I’m not entirely surprised by this outcome. In an ecosystem consisting primarily of back-end developers, the level of attention to UI concerns regarding a11y just isn’t there. Hopefully, over time, this will improve. But for now, the existing UI library ecosystem for Blazor is alarmingly ableist when compared to something like Shoelace.

Further, as mentioned above, the tooling for building accessible UI components is lacking. For example, in the JavaScript ecosystem, you have access to libraries and add-ons for your test runners like jest-axe and storybook-addon-a11y that can automate the testing of a number of accessibility related items in your unit tests or in your Storybook tests. To my knowledge, there aren’t such equivalents in Blazor’s ecosystem. Testing of accessibility is still primarily manual and / or part of end-to-end tests.

This isn’t to say of course that the accessibility posture won’t improve in this ecosystem over time. But for now, these are some important limitations.

Conclusion

Blazor is a pretty cool piece of technology and a capable framework for building web applications. Its strengths come from .NET and C#, which together make it both performant and production-grade with type safety that should also help make your codebase more resilient over time. If you have a team comprised primarily of C# developers, Blazor may be the right tool for you.

Notwithstanding all that it has to offer as a technology, the ecosystem it inhabits currently has less to offer UI devs than the existing JavaScript ecosystem, particularly in tooling for design systems and accessibility.

I am hopeful that this might change over time. But as with most things, time and effort will be the final arbiter.

  1. Razor is a syntax for combining HTML markup with C# code. Razor allows you to switch between HTML markup and C# in the same file with IntelliSense programming support in Visual Studio.
  2. However, in practice, limitations with IntelliSense, namespacing and a poor developer experience in Visual Studio Code mean that .razor files typically contain basic templating for a component with the logic being placed in an accompanying partial class file (e.g. Button.razor.cs).
  3. There is a client-side WASM mode if you want to return to early 2000s Macromedia Flash and have your users download large binaries before being able to see any content. Which is to say, that’s not likely something you want to reach for and not where the future of Blazor will be, in my view.
  4. You can learn more about server-side Blazor and circuits over SignalR here.
  5. How to properly use async / await with Task and not void, for example.
  6. There is a Visual Studio for Mac IDE but it is being retired on August 31, 2024.
  7. The Blazor documentation examples actually have you just dumping script tags on your base index.html that attach to the window object. Not exactly scalable.
  8. See the notes from my Changelog for October 2023, wherein I mentioned Stephanie Zeng's presentation on the use of this practice at Rangle.io.

Nice, you made it to the end of this post. Thanks for reading. Other posts by me are over on my writing page. If you'd like to subscribe to this blog, you can do so via RSS or Email Newsletter. If you found this post helpful, and want to support more of this content, you can also buy me a coffee.