A reusable Blazor modal component lets one dialog render different headers, forms, and actions without duplicating the modal shell on every page. This guide builds that component with a generic model, templated content, EditForm validation, and Bootstrap styling, then shows how to use it for create and edit workflows.

Version note: This article and its demo were originally created and verified with .NET 8 in June 2024. The accompanying video shows the working implementation at that time. Current framework or cloud-service versions may require changes; review the compatibility notes below before using it in production.

The source remains unchanged from the original demonstration. You can compare every file in the Blazor modal component repository and watch the original result video. This refresh improves the explanation and production guidance; it does not pretend that the historical code was rebuilt on a newer framework.

The original .NET 8 demonstration recorded in June 2024.

What the Blazor modal component does

The component owns the parts that every modal needs: visibility, size, a dialog shell, form state, and close and submit operations. The calling page supplies the parts that vary through three typed templates:

  • HeaderTemplate renders the title area.
  • BodyTemplate renders fields or other content.
  • FooterTemplate renders actions such as Cancel, Create, or Update.

The generic TItem value flows into each template as its context. The same modal shell can therefore display an Account, another form model, or a read-only object without hard-coding that model into the component. This is the same templated-component pattern described in current Microsoft documentation for RenderFragment<TValue>.

This component is broader than a confirmation prompt. If your page only needs a yes-or-no decision, the smaller Blazor confirm dialog component may be a better fit.

Project structure and runtime requirements

The original solution contains a Razor class library named Dnc.Common.Razor and a Blazor Web App named Dnc.Modal.WebApp. Both projects target net8.0. The class library references Microsoft.AspNetCore.Components.Web version 8.0.5, and the web app references the class library.

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>disable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>


  <ItemGroup>
    <SupportedPlatform Include="browser" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.5" />
  </ItemGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

</Project>

The page uses C# event handlers, binding, and form validation, so it must run in an interactive render mode. The original app enables Interactive Server components in Program.cs and applies InteractiveServer to HeadOutlet and Routes in App.razor. If these controls render but do not respond, verify the render-mode configuration before debugging the modal itself.

Create Dnc.Common.Razor/Modal/DncModalComponent.cs. The following is the original repository file, preserved without modernization:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Threading.Tasks;

namespace Dnc.Common.Razor.Modal
{
    public class DncModalComponent<TItem> : ComponentBase
    {
        [Parameter] public RenderFragment<TItem> HeaderTemplate { get; set; }
        [Parameter] public RenderFragment<TItem> BodyTemplate { get; set; }
        [Parameter] public RenderFragment<TItem> FooterTemplate { get; set; }

        [Parameter] public EventCallback<TItem> OnSubmit { get; set; }
        [Parameter] public EventCallback<TItem> OnShow { get; set; }
        [Parameter] public string Size { get; set; }

        public EditContext EditContext { get; protected set; }
        protected TItem Item { get; set; }

        protected bool IsVisible { get; set; }
        protected string ModalSize { get; set; } = string.Empty;

        public void SetEditContext(EditContext editContext)
        {
            EditContext = editContext;
        }

        public async Task Show(TItem item = default)
        {
            ModalSize = Size switch
            {
                "Small" => "modal-sm",
                "Medium" => string.Empty,
                "Larg" => "modal-lg",
                "ExtraLarg" => "modal-xl",
                _ => string.Empty,
            };

            IsVisible = true;
            Item = item;

            var task = OnShow.InvokeAsync(Item);
            if (task != null && !task.IsCompleted)
            {
                StateHasChanged();
                await task;
            }

            EditContext ??= new EditContext(new { });

            StateHasChanged();
        }
        public async Task HandleSubmit()
        {
            await OnSubmit.InvokeAsync(Item);
        }
        public void Close()
        {
            IsVisible = false;
            Item = default;
            EditContext = null;

            StateHasChanged();
        }
    }
}

Show selects a Bootstrap size class, stores the current item, makes the component visible, and invokes the caller’s OnShow callback. The caller can use that callback to supply an EditContext for the selected item. HandleSubmit forwards the form submission, while Close clears the item and form state.

Notice that the size values include the original spellings Larg and ExtraLarg. They are retained because changing them would change the demo’s public parameter contract. Treat those values as historical API names when following this repository.

Create Dnc.Common.Razor/Modal/DncModal.razor. It inherits from the base class, renders only while visible, and passes the current item to each template.

@inherits DncModalComponent<TItem>
@typeparam TItem

@if(IsVisible){
    @if(EditContext != null){
        <EditForm EditContext="@EditContext" OnSubmit="@HandleSubmit">
            <div class="modal fade show dnc-modal-background" id="DncModal" style="display: block;" aria-modal="true" role="dialog">
                <div class="modal-dialog @ModalSize">
                    <div class="modal-content">
                        <div class="modal-header">
                            @if (HeaderTemplate != null)
                            {
                                @HeaderTemplate(Item)
                            }
                            else
                            {
                                <h5 class="modal-title">Header</h5>
                            }

                            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="Close"></button>
                        </div>
                        <div class="modal-body">
                            @BodyTemplate(Item)
                        </div>
                        <div class="modal-footer">
                            @FooterTemplate(Item)
                        </div>
                    </div>
                </div>
            </div>
        </EditForm>
    }
}

The form uses OnSubmit, which runs for valid and invalid form states. Validation is therefore performed explicitly in the page handler with EditContext.Validate(). That behavior matches Microsoft’s documented distinction between OnSubmit, OnValidSubmit, and OnInvalidSubmit.

Add the original isolated stylesheet at Dnc.Common.Razor/Modal/DncModal.razor.css:

.modal-dialog {
    margin-top: 7rem;
}

.dnc-modal-background {
    background-color: rgba(0, 0, 0, 0.4);
    backdrop-filter: blur(15px);
}

The web app also loads Bootstrap 5.3.2 from a CDN in the original App.razor. Without Bootstrap’s CSS, the modal classes do not provide the expected layout. The component controls visibility itself, so this sample does not require Bootstrap’s JavaScript modal API.

Use the modal in a Blazor page

Add @using Dnc.Common.Razor.Modal to the web app’s _Imports.razor. The page keeps component references for separate Add and Edit instances. When a modal opens, the page assigns an EditContext; when the form submits, the page validates that context and performs its operation.

protected DncModal<Account> EditModal { get; set; }

protected DncModal<Account> AddModal { get; set; }

protected void EditModalSubmitted(Account account)
{
    var exist = Accounts.FirstOrDefault(v => v.Id == account.Id);

    if (exist != null && EditModal.EditContext.Validate())
    {
        // Update the account in real projects
        EditModal.Close();
    }
}

protected void EditModalDisplayed(Account account)
{
    EditModal.SetEditContext(new EditContext(account));
}

protected void AddModalSubmitted(Account account)
{
    var exist = Accounts.FirstOrDefault(v => v.Id == account.Id);

    if (exist == null && AddModal.EditContext.Validate())
    {
        Accounts.Add(account);
        AddModal.Close();
    };
}

protected void AddModalDisplayed(Account account)
{
    AddModal.SetEditContext(new EditContext(account));
}

The page calls EditModal.Show(account) for an existing row and AddModal.Show(AccountModel) for the create workflow. Each component instance receives its own header, body, and footer:

<DncModal TItem="Account"
          @ref="AddModal"
          OnSubmit="AddModalSubmitted"
          OnShow="AddModalDisplayed"
          Size="Larg">

    <HeaderTemplate>
        <h5>Create a new Account</h5>
    </HeaderTemplate>

    <BodyTemplate>
        <DataAnnotationsValidator />
        <div class="mb-3">
            <label for="Name" class="form-label">Name</label>
            <InputText class="form-control" id="Name" @bind-Value="AccountModel.Name" />
            <ValidationMessage For="() => context.Name"></ValidationMessage>
        </div>
        <div class="mb-3">
            <label for="Email" class="form-label">Email address</label>
            <InputText class="form-control" id="Email" @bind-Value="AccountModel.Email" placeholder="name@example.com" />
            <ValidationMessage For="() => AccountModel.Email"></ValidationMessage>
        </div>
        <div class="mb-3">
            <label for="Age" class="form-label">Age</label>
            <InputNumber class="form-control" id="Age" @bind-Value="AccountModel.Age" />
            <ValidationMessage For="() => AccountModel.Age"></ValidationMessage>
        </div>
        <div class="mb-3">
            <label for="ExpireDate" class="form-label">Expire Date</label>
            <InputDate class="form-control" id="ExpireDate" @bind-Value="AccountModel.ExpireDate" />
        </div>
    </BodyTemplate>

    <FooterTemplate>
        <button class="btn btn-secondary px-3" @onclick="()=>AddModal.Close()">Cancel</button>
        <button class="btn btn-success px-3" type="submit">Create Account</button>
    </FooterTemplate>
</DncModal>

This excerpt is copied from the original Home.razor. The complete Add and Edit examples remain in the repository. Keeping the repository as the source of truth avoids silently rewriting code that was demonstrated in the original video.

Verify the original demo behavior

The original video is evidence of the behavior in the 2024 .NET 8 environment. When reproducing that environment, verify the user task rather than only checking that the project starts:

  1. Open the home page and confirm that the account table renders.
  2. Select Create Account and confirm that the Add modal becomes visible.
  3. Submit missing or invalid values and confirm that Data Annotations messages appear.
  4. Enter valid values, submit, and confirm that the account is added and the modal closes.
  5. Select Edit Account for an existing row and confirm that the selected model fills the Edit modal.
  6. Close each modal with both the close button and the Cancel action.

If a current SDK produces different results, record the SDK version, browser, render mode, console errors, and exact failing step. Do not silently label the historical demo “current” after only opening the page.

Production checks before adoption

Keyboard and assistive-technology behavior

The preserved markup sets role="dialog" and aria-modal="true", but those attributes are only part of an accessible modal. The W3C modal-dialog pattern also expects focus to move into the dialog, Tab navigation to remain inside it, Escape to close it, and focus to return to the invoking control afterward. The original repository does not visibly implement those behaviors.

Before production use, test keyboard-only navigation and a screen reader. Give the dialog an accessible name, ensure the close control is reachable, decide the safest initial focus target, and prevent background content from behaving as if it were still active.

State and validation

  • The Add workflow reuses AccountModel. Decide whether a fresh model should be created after a successful submission or cancellation.
  • The Edit workflow receives the selected object directly. In a real application, decide whether edits should mutate the table immediately or operate on a copy until the user confirms.
  • OnSubmit runs for valid and invalid forms. Keep the explicit EditContext.Validate() call, or deliberately redesign around OnValidSubmit; do not mix the two approaches without understanding the event order.
  • Guard repeated Show calls and decide how the component behaves if another modal is already open.

Rendering and disconnects

The original sample uses Interactive Server, where browser events travel over a real-time connection to the server. Decide what the user sees if that circuit disconnects while a form is open. Also test prerendering and hydration if you move the component into a different render-mode arrangement.

Visual behavior

  • Test narrow screens, zoom, long validation messages, and content taller than the viewport.
  • Decide whether the document body should stop scrolling while the modal is open.
  • Check stacking order against navigation, cookie notices, and other overlays.
  • Confirm that external Bootstrap and icon CDN policies fit your Content Security Policy and availability requirements.

Current compatibility status

As of September 1, 2026, Microsoft lists .NET 10 as the active LTS release. The original project targets .NET 8, which is in maintenance and reaches end of support on November 10, 2026. That makes an upgrade assessment important for a new production system, but it does not erase the value of the original .NET 8 example.

The component architecture still aligns with current documentation: Razor components remain reusable UI units, generic RenderFragment<TValue> parameters still define templated content, and EditContext.Validate() remains a documented option for an OnSubmit handler. However, this refresh did not compile the repository with .NET 10, replace package versions, or change the source. Compatibility with newer targets is therefore unverified, not assumed.

Conclusion

The original Blazor modal component demonstrates a useful separation: the component owns dialog behavior and form state, while the calling page owns typed content and application actions. That makes one modal shell reusable across create and edit workflows.

Use the repository and video to understand the verified .NET 8 implementation. Before adopting it in production, evaluate the current framework target, complete the accessibility interaction model, and test state, validation, disconnection, and responsive behavior in your own application.

Official references

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
100% Free SEO Tools - Tool Kits PRO