Implementing Reliable Modals in Blazor

Ruslan Dudchenko
Ruslan Dudchenko
20 May 2024

4 min read

Introduction

Modals are a ubiquitous feature in modern web development, providing a way to display content overlaying the main application without navigating away from the current page. In Blazor, implementing modals effectively is crucial for delivering a seamless user experience. However, developers often encounter various challenges when working with modals in Blazor, such as positioning issues, handling scrollable content, and managing component hierarchies.
In this article, we delve into these common problems and present a robust solution developed by our team. By leveraging a parallel Blazor rendering tree, our approach eliminates the typical pitfalls associated with modals, ensuring they are easily positioned relative to the window and free from truncation. This method not only simplifies the development process but also enhances the overall performance and maintainability of Blazor applications.

Understanding the Problem

While developing in Blazor for an extended period, our team frequently encountered the need to display modal dialogs to users. The most straightforward method to achieve this is by creating an HTML element with absolute positioning within the component where the modal is required. However, this approach presents several significant challenges.
Firstly, positioning a modal relative to the center of the window can be quite difficult. Absolute positioning within a component often leads to misalignment issues, making it hard to ensure that the modal appears correctly centered on the screen. Secondly, if the modal is a part of a scrollable element, it may be truncated by the ‘overflow’ property. This truncation occurs when the modal extends beyond the visible area of the scrollable container, causing parts of it to be hidden from the user.
To mitigate these issues, a common solution is to place the modal higher in the component hierarchy. However, this method introduces a new set of problems. Adding the modal at a higher level often requires creating numerous redundant variables and callbacks to manage the modal’s state and behavior. This not only complicates the codebase but also leads to confusion among developers who need to navigate through unnecessary complexity.
In response to these challenges, our team devised a more efficient solution within our component library. We render all modals in a parallel Blazor rendering tree. This approach allows us to position modals relative to the window borders easily and ensures they are never truncated by any other elements on the page. By avoiding the pitfalls of absolute positioning and hierarchical complexity, our method provides a cleaner, more reliable way to implement modals in Blazor applications.

Our Solution and Implementation Details

Our solution to addressing modal challenges in Blazor involves creating a ModalComponent that effectively manages modal dialogs. This component registers the modal's markup, along with parameters such as width, height, and position, into a list managed by a service injected via dependency injection (DI). When changes occur in the modal, this list of modals is re-rendered in a parallel rendering tree, ensuring proper positioning and visibility.
The ModalModel includes parameters related to the positioning and dimensions of the modal, a callback for updating the modal, and a reference to the RenderFragment representing the modal's content. Here is the code snippet of the ModalModel class:
ModalModel.cs
public class ModalModel
{
    public string Id { get; set; } = $"_id_{Guid.NewGuid()}";
    public string Width { get; set; } = string.Empty;
    public string Height { get; set; } = string.Empty;
    public string X { get; set; } = string.Empty;
    public string Y { get; set; } = string.Empty;
    public string CssClass { get; set; } = string.Empty;
    public int ZIndex { get; set; } = 999999;
    public RenderFragment? Fragment { get; set; }
    public Action? OnUpdate { get; set; }
}
The ModalService manages the list of modals and provides the necessary methods for adding, updating, and removing modals:
IModalService.cs
public interface IModalService
{
    IReadOnlyList<ModalModel> Modals { get; }
    event Action? OnUpdate;

    void Open(ModalModel modalModel);
    void Close(ModalModel modalModel);
}
ModalService.cs
public class ModalService : IModalService
{
    public IReadOnlyList<ModalModel> Modals { get; }
    public event Action? OnUpdate;

    private readonly List<ModalModel> _modals = new();

    public ModalService()
    {
        Modals = _modals;
    }

    public void Open(ModalModel modalModel)
    {
        if (_modals.Contains(modalModel)) return;
        
        _modals.Add(modalModel);
        OnUpdate?.Invoke();
    }

    public void Close(ModalModel modalModel)
    {
        if (!_modals.Contains(modalModel)) return;
        
        _modals.Remove(modalModel);
        OnUpdate?.Invoke();
    }
}
The RootModalComponent is responsible for looping through the list of modals and rendering them:
RootModalComponent.razor
@foreach (var modalModel in ModalService.Modals)
{
    <ModalContentComponent @key="@modalModel" ModalModel="modalModel"/>
}
RootModalComponent.razor.cs
using Microsoft.AspNetCore.Components;

public partial class RootModalComponent : IDisposable
{
    [Inject] private IModalService ModalService { get; set; }

    protected override void OnInitialized()
    {
        ModalService.OnUpdate += StateHasChanged;
    }

    public void Dispose()
    {
        ModalService.OnUpdate -= StateHasChanged;
    }
}
The ModalContentComponent handles the rendering of each modal, specifying its position, dimensions, and other useful parameters such as CssClass and ZIndex:
ModalContentComponent.razor
@{
    var inCenter = InCenter();
}

<div @onclick:stopPropagation
     id="@ModalModel.Id"
     class="@($"bch-modal-content-container {(inCenter ? "bch-central" : "")} {ModalModel.CssClass}")"
     style="@($"width: {ModalModel.Width}; height: {ModalModel.Height}; z-index: {ModalModel.ZIndex}; " + $"{(!inCenter ? $"left: {ModalModel.X}; top: {ModalModel.Y};" : "")}")">
    @ModalModel.Fragment
</div>

<style>
    .bch-modal-content-container {
        display: flex;
        position: absolute;
    }
    
        .bch-modal-content-container.bch-central {
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
        }
</style>
ModalContentComponent.razor.cs
using Microsoft.AspNetCore.Components;

public partial class ModalContentComponent : IDisposable
{
    [Inject] private IModalService ModalService { get; set; }
    [Parameter] public ModalModel ModalModel { get; set; } = new ();
    
    protected override void OnInitialized()
    {
        ModalModel.OnUpdate += StateHasChanged;
    }
    
    public void Dispose()
    {
        ModalModel.OnUpdate -= StateHasChanged;
    }
    
    private bool InCenter()
    {
        return string.IsNullOrWhiteSpace(ModalModel.X) || string.IsNullOrWhiteSpace(ModalModel.Y);
    }
}
The ModalComponent is the actual component used to render our modal. It maps the component's parameters to our ModalModel and binds the component's lifecycle with the ModalService to show and hide the modal. Here is the implementation:
ModalComponent.cs
using Microsoft.AspNetCore.Components;

public class ModalComponent : ComponentBase, IDisposable
{
    [Inject] private IModalService ModalService { get; set; }

    [Parameter] public RenderFragment ChildContent { get; set; }

    [Parameter] public string Id { get; set; } = "";
    [Parameter] public string Width { get; set; } = "200px";
    [Parameter] public string Height { get; set; } = "200px";
    [Parameter] public string X { get; set; } = "";
    [Parameter] public string Y { get; set; } = "";
    [Parameter] public string CssClass { get; set; } = string.Empty;
    [Parameter] public int ZIndex { get; set; } = 999999;

    private readonly ModalModel _modalModel = new();

    [Parameter] public EventCallback<bool> ShowChanged { get; set; }
    [Parameter] public bool Show
    {
        get => _show;
        set
        {
            if (_show == value) return;
            _show = value;

            ResetValuesFromParameters();

            if (_show) ModalService.Open(_modalModel);
            else ModalService.Close(_modalModel);

            ShowChanged.InvokeAsync(value);
        }
    }

    private bool _show;

    private string _prevX = string.Empty;
    private string _prevY = string.Empty;
    private string _prevWidth = string.Empty;
    private string _prevHeight = string.Empty;
    private string _prevCssClass = string.Empty;
    private int _prevZIndex = 999999;

    protected override void OnInitialized()
    {
        ResetValuesFromParameters();

        if (Show) ModalService.Open(_modalModel);
        else ModalService.Close(_modalModel);
    }

    protected override void OnAfterRender(bool firstRender)
    {
        _modalModel.OnUpdate?.Invoke();
    }

    protected override void OnParametersSet()
    {
        if (_prevX != X || _prevY != Y || _prevZIndex != ZIndex || _prevWidth != Width ||
            _prevHeight != Height || _prevCssClass != CssClass) Update();
    }

    public void Dispose()
    {
        ModalService.Close(_modalModel);
    }

    public void Update()
    {
        ResetValuesFromParameters();
        _modalModel.OnUpdate?.Invoke();
    }

    private void ResetValuesFromParameters()
    {
        if (!string.IsNullOrEmpty(Id)) _modalModel.Id = Id;

        _modalModel.Fragment = ChildContent;
        
        _modalModel.X = X;
        _modalModel.Y = Y;
        _modalModel.Width = Width;
        _modalModel.Height = Height;
        _modalModel.CssClass = CssClass;
        _modalModel.ZIndex = ZIndex;

        _prevX = _modalModel.X;
        _prevY = _modalModel.Y;
        _prevWidth = _modalModel.Width;
        _prevHeight = _modalModel.Height;
        _prevCssClass = CssClass;
        _prevZIndex = ZIndex;
    }
}
To connect the modal to a Blazor WebAssembly application, you need to set up the ModalService and RootModalComponent in the Program.cs file:
Program.cs
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorModal.Modal;
using BlazorModal.Modal.Components.Root;
using BlazorModal.WebAssemblyApp;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.RootComponents.Add<RootModalComponent>("body::after");

var services = builder.Services;
services.AddScoped<IModalService, ModalService>();

await builder.Build().RunAsync();
Finally, here is an example of how to use the modal in your application:
IndexPage.razor
@page "/"

<div class="page">
    <button @onclick="OnOpenModalInCenterClicked">Open Modal in center of the screen</button>
    <button @onclick="OnOpenModalWithCoordinatesClicked">Open Modal with coordinates</button>
</div>

<ModalComponent @bind-Show="_showModal"
                Width="300px"
                Height="200px"
                X="@_xPos"
                Y="@_yPos">
    <div class="modal-container">
        <button @onclick="OnCloseModalClicked">Close</button>
    </div>
</ModalComponent>
IndexPage.razor.cs
public partial class IndexPage
{
    private bool _showModal = false;
    private string _xPos = string.Empty;
    private string _yPos = string.Empty;

    private void OnOpenModalInCenterClicked()
    {
        _xPos = string.Empty;
        _yPos = string.Empty;
        _showModal = true;
    }
    
    private void OnOpenModalWithCoordinatesClicked()
    {
        _xPos = "calc(100% - 350px)";
        _yPos = "100px";
        _showModal = true;
    }

    private void OnCloseModalClicked()
    {
        _showModal = false;
    }
}
IndexPage.razor.css
.page {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

    .page button {
        width: fit-content;
    }

.modal-container {
    width: 100%;
    height: 100%;
    border-radius: 15px;
    background: white;
    box-shadow: 0 0 8px #00000040;

    display: flex;
    justify-content: center;
    align-items: center;
}

Results

Conclusion and Further Resources

Thank you for reading about our solution for managing modals in Blazor. We hope you found this approach insightful and helpful for your own Blazor projects. To explore the source code and see this implementation in action, you can visit our GitHub repository here.
Additionally, we invite you to explore our comprehensive component library, where you can find a wide array of components designed to simplify and enhance your Blazor development experience. Our library includes the Modal component discussed in this article, packed with even more features to cater to various use cases.
For more information and to start integrating these components into your projects, check out our component library here. We are continually updating and expanding our library, so stay tuned for more powerful tools and enhancements.
Feel free to connect with me on LinkedIn for updates, discussions, and more insights on Blazor and other development topics. You can find my LinkedIn profile here. I welcome any questions or feedback, and I’m always open to networking with fellow developers.
Thank you for your time, and happy coding!

You may also read

Generating Code 128C Barcodes in Blazor: A Step-by-Step Guide
This article delves into the implementation of barcode rendering, focusing specifically on Code 128C, using Blazor and C#. The goal is to generate barcodes entirely within a Blazor application by implementing the encoding algorithm in C#. While this article primarily focuses on Code 128C, the same algorithmic approach can be adapted to render other types of barcodes, making it a versatile solution for various use cases.
Ruslan Dudchenko
Ruslan Dudchenko
22 Nov 2024
Building and Releasing a .NET MAUI App to Google Play Using Azure DevOps YAML Pipelines
Publishing a mobile app to the Google Play for the first time can be difficult. Developers often face questions about certificates, signing, store rules, and how everything works together in a CI/CD pipeline. When you add .NET MAUI and cloud builds to this process, it can become confusing, especially without clear documentation.
Ruslan Dudchenko
Ruslan Dudchenko
18 Jan 2026
Building and Releasing a .NET MAUI App to App Store Using Azure DevOps YAML Pipelines
Publishing a mobile app to the Apple App Store can be challenging, especially for developers doing it for the first time. Apple requires specific certificates, provisioning profiles, and strict signing rules, and all of them must be configured correctly to work in an automated CI/CD pipeline. Understanding how these pieces fit together in a cloud build environment is often difficult, as the required information is spread across multiple Apple and DevOps resources.
Ruslan Dudchenko
Ruslan Dudchenko
18 Jan 2026

What our clients say

Senior Angular Developer

Aug 11, 2025

Need help fixing an Azure Key Vault issue in Blazor Server

Apr 3, 2025

Need help fixing Graph connection issue in Blazor Server (prod env)

Mar 11, 2025

C# Blazor + EF Core Tutor — 1:1 mentoring / pair debugging

Feb 13, 2026

Azure Pipeline for .Net Maui

Jan 4, 2025

Epic Solutions Grocery project

Dec 9, 2024

.NET C# Blazor Developer for CRM Rewrite

Oct 6, 2022

Excellent work from start to finish. The developer resolved all problems in our Angular app and delivered new features without any fuss. Professional, reliable, and highly skilled.

FAQ

Do you provide support after launch?

Yes, we provide post-launch support, including maintenance, updates, bug fixes, and product improvements as needed.

How long does it take to develop a website or an app?

The timeline depends on the project complexity. On average: a website takes 2–6 weeks, and an app takes 2–4 months.

Can you help if I don’t have a clear idea yet?

Yes, we help shape your idea, define features, and create a product concept.

How much does project development cost?

The cost depends on the scope and complexity. We provide a custom estimate after discussing your requirements.

Can I order a redesign of an existing website or app?

Yes, we offer redesign services, improve UX/UI, and update the functionality of existing products. We also have strong experience working with legacy systems and complex codebases.

Is it possible to order only design or development?

Yes, you can order design or development separately based on your needs.

Which countries do you work with?

We work with clients worldwide and have experience with international projects.

Let’s team up!

Fill out the form to get in touch

By clicking this button I accept Privacy Policy of this site.

Got an idea?

We're all ears 😊

Drop us a line

admin@vertexcode.dev