Rethinking Everyday UI: Building an Inline Text Editor with Raw HTML and Blazor


When I share these raw HTML controls for Blazor, the goal isn't just to walk through C# syntax or show off lifecycle methods. It’s about taking a step back to look at the types of UI controls you can actually create when you strip away heavy third-party component libraries and lean into native web standards.

Too often, we reach for massive UI suites or build clunky, full-page forms just to update a single piece of text. If a user just wants to rename a project, tweak a purchase-request description, or fix a typo on a dashboard label, forcing them into a modal or a separate edit screen is overkill. It breaks their flow.

Instead, we can build a clean, accessible Inline Text Editor using nothing but standard HTML elements and a bit of Blazor state management. Click the text, edit it right where it lives, and either save it or walk away.

Designing for Reusability (Without the Side Effects)

A good UI control shouldn't care what it is editing, and it definitely shouldn't mutate your application state while the user is still typing.

At the surface level, the component just needs a label, the current value, a max length, and an EventCallback<string> to fire when the user commits their change:

<InlineTextEditor Label="Project name"
                  Value="@_projectName"
                  ValueChanged="SaveProjectName" />

The real trick to making an inline editor feel solid is isolating the work-in-progress. When the user clicks the text to start editing, we copy Value into a private _draft variable. Two-way binding happens strictly against that draft:

private void BeginEdit()
{
    _draft = Value;
    _error = null;
    _editing = true;
    _focusPending = true;
}

Because we aren't touching the parent's data on every keystroke, canceling is trivial. Whether the user clicks the Cancel button or hits the Escape key, we just flip _editing back to false and throw the draft away. The parent component never even knows it happened.

Explicit Saves and Graceful Failures

A lot of inline editors try to be clever by auto-saving on onblur (when the user clicks or tabs away). In practice, that usually leads to accidental commits when someone just meant to switch windows. I strongly prefer an explicit Save button.

When the user clicks Save, the control does some basic sanity checking—trimming whitespace and making sure the string isn't empty or blowing past MaxLength. Once it passes, we lock the UI with a _saving flag so impatient double-clickers can't fire off duplicate requests:

if (_saving) return;

var candidate = _draft.Trim();
if (candidate.Length == 0 || candidate.Length > MaxLength)
{
    _error = $"Enter between 1 and {MaxLength} characters.";
    _focusPending = true;
    return;
}

_saving = true;
try
{
    await ValueChanged.InvokeAsync(candidate);
    _editing = false;
}
catch (Exception)
{
    _error = "Could not save. Your draft is still here; try again.";
}
finally
{
    _saving = false;
    _focusPending = true;
}

Notice how the component doesn't know how the data gets saved; the parent owns the persistence:

private string _projectName = "Warehouse Expansion";

private async Task SaveProjectName(string candidate)
{
    await projectRepository.SaveNameAsync(candidate);
    _projectName = candidate;
}

If that database call or API request blows up, the exception bubbles back to our editor's catch block. Instead of crashing the app or losing what the user typed, the editor stays open, keeps their draft intact, and displays a friendly retry message. (Just remember to log the actual exception in your persistence layer—never dump raw stack traces into the UI.)

Managing Focus Without JavaScript

One detail that separates a toy component from a production-ready control is focus management. When you click the text to edit it, your cursor should land straight inside the <input>. When you save or cancel, focus needs to snap right back to the display button so keyboard users don't get lost at the top of the DOM.

Since the <input> only renders when _editing is true, we use OnAfterRenderAsync to shift focus the moment the DOM updates:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (!_focusPending) return;
    _focusPending = false;

    if (_editing)
        await _input.FocusAsync();
    else
        await _trigger.FocusAsync();
}

Best of all, there's zero JS interop to maintain here. Blazor's built-in ElementReference.FocusAsync handles the heavy lifting natively.

Letting Native HTML Handle Accessibility

When you build with raw HTML, you get a massive amount of accessibility for free—provided you use the right tags.

Instead of attaching @onclick events to <div> or <span> tags, use native <button> elements for the display trigger, Save, and Cancel. Right out of the box, the browser gives you Tab navigation plus Enter and Space activation.

For the input itself, we pair standard HTML attributes with ARIA properties so screen readers know immediately if a save fails:

<input id="@_id"
       @bind="_draft" @bind:event="oninput"
       maxlength="@MaxLength"
       disabled="@_saving"
       aria-invalid="@(_error is not null ? "true" : "false")"
       aria-describedby="@($"{_id}-message")" />

<span id="@($"{_id}-message")"
      role="status" aria-live="polite">
    @_error
</span>

One architectural tip: keep this control outside of an enclosing <form>. It’s designed as a standalone micro-interaction for updating a single value on the fly, not part of a larger batch submission.

Putting It to the Test

In the sample project, I wired this single component up to three different scenarios: a short project title, a longer purchase-request description, and a dashboard label, each with its own character limit. I also threw in a couple of test toggles to simulate slow network latency and database failures.

Watching the buttons disable during a slow save—or seeing your typed text survive a simulated server crash—really drives home the point. You don't need a bloated component library to build resilient, keyboard-friendly UI controls. Plain HTML and a clean state machine get the job done.


[source code]

Comments

Popular posts from this blog

Customizing PWA Manifest and Icons for a Polished User Experience 🚀

Offline-First Strategy with Blazor PWAs: A Complete Guide 🚀

Yes, Blazor Server can scale!