HTML Invoker Commands in Blazor
Stop Using JS for Things the Browser Already Handles
Blazor makes handling UI events in C# almost effortless. Because of that, it’s tempting to default to @onclick, component state flags, conditional rendering, and JS Interop for every basic interaction.
But modern HTML has caught up. The browser now handles many of the small UI toggles we used to build manually.
One of the most useful recent additions to web standards is HTML Invoker Commands.
Invoker Commands allow an HTML element (usually a button) to declaratively tell another element to perform an action. For example, opening a modal:
<button commandfor="detailsDialog" command="show-modal">
View Details
</button>
Notice what’s missing here:
No JavaScript event listeners
No Blazor
@onclickhandlersNo
bool _isOpenflags tracking visibilityNo JS Interop calls
The browser manages the interaction directly. For Blazor developers, this brings up an important architectural question: If the browser natively understands an interaction, should Blazor be involved at all?
More often than not, the answer is no.
How Invoker Commands Work
The API relies on two primary HTML attributes:
commandfor: Identifies the target element by itsid.command: Specifies the action to execute.
Here is a minimal modal setup:
<button commandfor="myDialog" command="show-modal">
Open Dialog
</button>
<dialog id="myDialog">
<h2>Hello!</h2>
<button commandfor="myDialog" command="close">
Close
</button>
</dialog>
When clicked, the browser executes show-modal on #myDialog. Clicking Close executes close.
Under the hood, this replaces boilerplate JavaScript like document.getElementById('myDialog').showModal(), but without writing scripts or managing element references.
Why This Matters for Blazor Architecture
Every field added to a Razor component increases its mental overhead and maintenance cost.
Consider how we usually handle a dialog in Blazor:
<button class="btn btn-primary" @onclick="ShowDetails">
View Details
</button>
@if (_showDetails)
{
<div class="modal-backdrop">
<div class="project-dialog">
<h2>Project Phoenix</h2>
<p>Modernize the company's project management platform.</p>
<button class="btn btn-secondary" @onclick="HideDetails">
Close
</button>
</div>
</div>
}
@code {
private bool _showDetails;
private void ShowDetails() => _showDetails = true;
private void HideDetails() => _showDetails = false;
}
None of this code contains actual business logic. It exists solely to manage local UI presentation state.
We can separate responsibilities cleanly:
Browser Behavior ──> HTML / Native APIs
Application Logic ──> Blazor / C#
If we swap this for native HTML dialogs and Invoker Commands, the Blazor code disappears completely:
<button class="btn btn-primary" commandfor="projectDetails" command="show-modal">
View Details
</button>
<dialog id="projectDetails">
<h2>Project Phoenix</h2>
<p>Modernize the company's project management platform.</p>
<button class="btn btn-secondary" commandfor="projectDetails" command="close">
Close
</button>
</dialog>
No state, no re-renders, no @onclick.
Popover Menus Without JS Libraries
Beyond <dialog>, Invoker Commands shine when paired with the native Popover API.
Instead of importing Bootstrap's JS or pulling in a heavy UI component library for a dropdown menu, you can pair popover attributes with command triggers:
<button commandfor="projectActions" command="toggle-popover">
Actions
</button>
<div id="projectActions" popover>
<button>Edit</button>
<button>Duplicate</button>
<button>Archive</button>
<button>Delete</button>
</div>
Supported built-in commands for popovers include show-popover, hide-popover, and toggle-popover. The browser handles backdrop clicks, dismissal on press of the Escape key, and positioning state out of the box.
Combining Native UI with Blazor Logic
Using Invoker Commands doesn't mean replacing Blazor; it means delegating UI mechanics to the browser while keeping your business rules in C#.
Take a delete confirmation dialog as an example:
Opening/closing the dialog is pure UI presentation (HTML handles this).
Executing the delete request is application logic (Blazor handles this).
<button class="btn btn-danger" commandfor="deleteDialog" command="show-modal">
Delete
</button>
<dialog id="deleteDialog">
<h2>Delete Project?</h2>
<p>Are you sure you want to delete Project Phoenix?</p>
<div class="dialog-actions">
<!-- Browser handles closing -->
<button class="btn btn-secondary" commandfor="deleteDialog" command="close">
Cancel
</button>
<!-- Blazor handles backend logic -->
<button class="btn btn-danger" @onclick="DeleteProject">
Confirm Delete
</button>
</div>
</dialog>
@code {
[Parameter] public int ProjectId { get; set; }
private async Task DeleteProject()
{
await ProjectService.DeleteAsync(ProjectId);
NavigationManager.NavigateTo("/projects");
}
}
What About Custom Commands?
Invoker Commands also support custom actions prefixed with --:
<button commandfor="projectCard" command="--archive">
Archive
</button>
To respond to a custom command, you attach a JavaScript event listener to the target element:
<script>
document.getElementById("projectCard").addEventListener("command", (event) => {
if (event.command === "--archive") {
// Handle custom action
}
});
</script>
Rule of thumb for Blazor: Avoid custom commands if their only purpose is to trigger C# code. Introducing custom JS handlers and Interop callbacks just to avoid @onclick adds unnecessary complexity. Stick to @onclick for custom application logic, and save Invoker Commands for native browser behaviors (show-modal, close, toggle-popover).
Built-in Accessibility
Building accessible overlay controls from scratch is notoriously tricky. A proper modal requires:
Focus trapping
Returning focus to the trigger element upon closing
Dismissal via the
EscapekeySetting appropriate ARIA roles and properties (
aria-expanded,aria-controls)
Native browser primitives (<dialog>, popover, and commandfor) handle these keyboard and screen reader relationships automatically. Reusing these primitives gives you better accessibility defaults without having to re-invent focus management.
Browser Support
The Invoker Commands API is part of Baseline 2025 and has broad support across modern versions of Chrome (v135+), Edge, Firefox, and Safari.
Internal/Enterprise apps: If your deployment target runs modern browser versions, you can start using this today.
Public/Legacy web apps: If you support older devices or locked-down corporate environments that update slowly, verify your user analytics or provide standard polyfills/fallbacks.
When to Stick with Blazor State
Native commands aren't a global replacement for framework features. Keep using standard Blazor components and @onclick when:
State affects other application logic: If whether a panel is open changes authorization checks, form validation, or adjacent layout components, Blazor needs to own that state (e.g.,
bool _isEditing).Using rich UI component suites: Complex data grids, date pickers, rich text editors, and multi-step wizard components rely on deep internal state machine logic that native HTML tags don't cover.
Execution clarity: Replacing a single
@onclick="Archive"handler with custom JS invoker events creates unnecessary indirection.
Summary: Let the Browser Do Its Job
Over the last decade, client-side frameworks began handling basic UI interactions because browsers lacked clean native APIs. Now that the standards have evolved, we can offload purely presentation-focused logic back to the browser.
Before writing your next component, ask a few quick questions:
Does application logic actually need to know this UI element opened?
Is this state real business data, or just temporary visual state?
Is there a native HTML API that covers this interaction?
Keep application logic in Blazor, let CSS handle presentation, and let HTML handle native interactions.

Comments
Post a Comment