Update: I've converted this answer into a service which can be found here.
I adjusted Kyles and grammophones answers to support our beloved Alert, Prompt, and Confirm from both C# and JavaScript. Tested in the lastest Blazor Server release with Bootstrap 5.
ProjectName.Components.Modal.razor
@using Microsoft.JSInterop
<div class="modal @ModalClass" tabindex="-1" role="dialog" style="display:@ModalDisplay; overflow-y: auto;">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title w-100 text-center" style="padding-left:31px">@Title</h5>
                <button type="button" class="close border-0 bg-white" data-dismiss="modal" aria-label="Close"  @onclick="() => Close(true)">
                    <span aria-hidden="true">×</span>
                </button>
            </div>
            <div class="modal-body mx-auto text-center">
                @Body
                @if (MType == ModalType.Prompt){ 
                    <input type="text" class="form-control text-center my-2" @bind-value="PromptValue" style="max-width:400px"></input> 
                }
            </div>
            <div class="modal-footer justify-content-center">
                @if (MType == ModalType.Prompt || MType == ModalType.Confirm)
                {
                    <button type="button" class="btn btn-secondary" data-dismiss="modal" @onclick="() => Close(false)">OK</button>
                    <button type="button" class="btn btn-secondary" data-dismiss="modal" @onclick="() => Close(true)">Cancel</button>
                }
                else
                {
                    <button type="button" class="btn btn-secondary" data-dismiss="modal" @onclick="() => Close(false)">Close</button>
                }
            </div>
        </div>
    </div>
</div>
@if (ShowBackdrop)
{
    <div class="modal-backdrop fade show"></div>
}
@code {
    [Inject] IJSRuntime JS { get; set; }
    public enum ModalType
    {
        Alert,
        Prompt,
        Confirm
    }
    /// <summary>
    /// (Optional) We can setup an instance of this .net object to call directly from JavaScript. See JavaScript Usage section.
    /// </summary>
    /// <returns></returns>
    protected override async Task OnInitializedAsync()
    {
        JS.InvokeVoidAsync("MODAL.SetDotnetReference", DotNetObjectReference.Create(this));
    }
    private string Title { get; set; }
    private string Body { get; set; }
    public Guid Guid = Guid.NewGuid();
    public string ModalDisplay = "none;";
    public string ModalClass = "";
    public bool ShowBackdrop = false;
    private string PromptValue { get; set; }
    private bool ConfirmValue { get; set; }
    private ModalType MType { get; set; }
    private List<string> MsgIds = new List<string>();
    [JSInvokable("Show")]
    public async Task<dynamic> Show(ModalType mType, string title, string body)
    {
        // The JavaScript call MODAL.DotNetReference.invokeMethodAsync is non-blocking
        // This means multiple calls to show the modal using invokeMethodAsync will only show the modal once.
        // We can solve this by making sure each message waits in line.
        string msgId = Guid.NewGuid().ToString();
        if (!MsgIds.Contains(msgId))
            MsgIds.Add(msgId);
        // If multiple messages are being processed, wait for this msgs turn.
        while (MsgIds.Count > 1 && MsgIds.IndexOf(msgId) != 0)
            await Task.Delay(250);
        Title = title;
        Body = body;
        ModalDisplay = "block;";
        ModalClass = "Show";
        MType = mType;
        ShowBackdrop = true;
        StateHasChanged();
        while (ShowBackdrop)
            await Task.Delay(250);
         switch(mType)
        {
            default:
            case ModalType.Alert:
                MsgIds.Remove(msgId);
                return string.Empty;
            case ModalType.Confirm:
                bool confirmResponse = ConfirmValue;
                MsgIds.Remove(msgId);
                return confirmResponse;
            case ModalType.Prompt:
                string promptResponse = PromptValue;
                MsgIds.Remove(msgId);
                return promptResponse;
        }
    }
    private void Close(bool isCancel)
    {
        // Determine returned values.
        PromptValue = isCancel ? string.Empty : PromptValue;
        ConfirmValue = isCancel ? false : true;
        ModalDisplay = "none";
        ModalClass = "";
        ShowBackdrop = false;
        StateHasChanged();
    }
}
Markup Usage
<Modal @ref="Modal"></Modal>
<button @onclick='() => Modal.Show(Modal.ModalType.Alert, "Title goes here","Body goes here")'>Open Modal</button>
Code Usage
if (await Modal.Show(Modal.ModalType.Confirm,"Save Settings", "Are you sure you want to save settings?"))
{
    string fileName = await Modal.Show(Modal.ModalType.Prompt, "File Name", "Please enter a filename");
    if (!string.IsNullOrEmpty(fileName))
        await Modal.Show(Modal.ModalType.Alert, "File Saved", $"File Saved as {fileName}");
}
JavaScript Usage
With promise support we can get a response from Prompt and Confirm right from JavaScript. To avoid declaring our Modal as static we need to setup a DotNetReference.
// Defined somewhere globally
var MODAL = {};
MODAL.DotNetReference = null;
MODAL.SetDotnetReference = function (pDotNetReference) {
    MODAL.DotNetReference = pDotNetReference;
};
MODAL.MType = {
    Alert: 0,
    Prompt:1,
    Confirm: 2,
};
// Called from wherever
MODAL.DotNetReference.invokeMethodAsync('Show', MODAL.MType.Prompt, `Title goes here`, `Body goes here`)
.then(data => {
    console.log(`Prompt Response`, data);
});
JavaScript Note: Polyfil recommended for promise support in older browsers