What if you need a button, badge, or alert component that appears across dozens of views with consistent styling? Instead of copy-pasting HTML, you create a Custom HTML Helper — a reusable C# method that generates HTML markup and is callable from any Razor view using @Html.YourMethod().
Custom HTML Helpers are static extension methods on the IHtmlHelper interface. They encapsulate repetitive HTML generation logic into reusable, testable C# methods. When called in a Razor view, they return IHtmlContent — properly encoded HTML that the Razor engine safely renders.
IHtmlHelper — The interface your extension method extendsTagBuilder — The class that generates well-formed, XSS-safe HTML tagsIHtmlContent — The return type that Razor renders without double-encoding_ViewImports.cshtml — Where you register your helper's namespace// Helpers/HtmlHelperExtensions.cs
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MyApp.Helpers
{
public static class HtmlHelperExtensions
{
/// <summary>
/// Renders a Bootstrap-styled status badge.
/// Usage: @Html.StatusBadge("Active", "success")
/// </summary>
public static IHtmlContent StatusBadge(this IHtmlHelper helper,
string text, string variant = "primary")
{
// TagBuilder ensures proper HTML encoding (XSS prevention)
var span = new TagBuilder("span");
span.AddCssClass($"badge bg-{variant} rounded-pill px-3 py-2");
span.InnerHtml.Append(text);
return span;
}
/// <summary>
/// Renders a delete confirmation button with data attributes.
/// Usage: @Html.DeleteButton("/api/products/5", "Delete Product")
/// </summary>
public static IHtmlContent DeleteButton(this IHtmlHelper helper,
string deleteUrl, string confirmMessage = "Are you sure?")
{
var button = new TagBuilder("button");
button.AddCssClass("btn btn-danger btn-sm");
button.Attributes.Add("data-delete-url", deleteUrl);
button.Attributes.Add("data-confirm", confirmMessage);
button.Attributes.Add("onclick", "confirmDelete(this)");
button.InnerHtml.AppendHtml("Delete");
return button;
}
/// <summary>
/// Renders a formatted price with currency symbol.
/// Usage: @Html.FormatPrice(49.99m, "USD")
/// </summary>
public static IHtmlContent FormatPrice(this IHtmlHelper helper,
decimal price, string currency = "USD")
{
var symbol = currency switch
{
"USD" => "$", "EUR" => "€", "GBP" => "£",
"INR" => "₹", "JPY" => "¥", _ => currency
};
var span = new TagBuilder("span");
span.AddCssClass("fw-bold text-success");
span.InnerHtml.AppendHtml($"{symbol}{price:N2}");
return span;
}
}
}
<!-- Views/_ViewImports.cshtml -->
@using MyApp.Helpers <!-- Makes @Html.StatusBadge(), etc. available in all views -->
<!-- Clean, reusable usage across any view -->
<h3>Order Status: @Html.StatusBadge(order.Status, order.IsActive ? "success" : "danger")</h3>
<td>@Html.FormatPrice(product.Price, "USD")</td>
<td>@Html.DeleteButton("/api/products/" + product.Id, "Delete " + product.Name + "?")</td>
/// Renders a Bootstrap card with header, body, and footer
public static IHtmlContent InfoCard(this IHtmlHelper helper,
string title, string body, string footer = null, string icon = "fas fa-info-circle")
{
var card = new TagBuilder("div");
card.AddCssClass("card shadow-sm mb-4");
// Card Header
var header = new TagBuilder("div");
header.AddCssClass("card-header bg-primary text-white");
header.InnerHtml.AppendHtml($"{title}");
card.InnerHtml.AppendHtml(header);
// Card Body
var cardBody = new TagBuilder("div");
cardBody.AddCssClass("card-body");
cardBody.InnerHtml.AppendHtml($"{body}
");
card.InnerHtml.AppendHtml(cardBody);
// Optional Footer
if (!string.IsNullOrEmpty(footer))
{
var cardFooter = new TagBuilder("div");
cardFooter.AddCssClass("card-footer text-muted small");
cardFooter.InnerHtml.Append(footer);
card.InnerHtml.AppendHtml(cardFooter);
}
return card;
}
| Feature | Custom HTML Helpers | Tag Helpers | View Components |
|---|---|---|---|
| Syntax | @Html.Method() | <tag asp-attr=""> | @await Component.InvokeAsync() |
| Returns | IHtmlContent | Modifies TagHelperOutput | Razor View (.cshtml) |
| DI Support | ❌ Static methods | ✅ Constructor injection | ✅ Full DI |
| Best For | Small, stateless UI snippets | Enhancing HTML elements | Complex components with data access |
| Complexity | Low | Medium | High |
TagBuilder — never concatenate raw HTML strings (XSS risk)IHtmlContent, not string — prevents double-encoding_ViewImports.cshtml for global availabilityQ: "How would you decide between creating a Custom HTML Helper vs a Tag Helper vs a View Component?"
Answer: "The decision comes down to complexity and data needs. For simple, stateless UI elements like badges, icons, or formatted prices — I use HTML Helpers. They're just static methods, fast to write, and have zero overhead. When I need to enhance existing HTML elements with server-side behavior — like adding asp-for binding — I use Tag Helpers because they feel natural in HTML. When the component needs its own data (e.g., a 'Recent Posts' sidebar that queries the database) — I use View Components, since they support full DI and have their own controller-like InvokeAsync method with a dedicated Razor view."