This guide explains how to ensure Blazor components render identical HTML to their ASP.NET Web Forms counterparts.
- CSS Compatibility - Existing stylesheets targeting Web Forms HTML structure continue to work
- JavaScript Compatibility - Client-side scripts that query or manipulate DOM elements remain functional
- Visual Consistency - Users see no visual difference after migration
- Test Verification - Enables automated comparison between Web Forms and Blazor output
The official documentation provides control rendering details:
- Base URL:
https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols?view=netframework-4.8 - Control-specific:
https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.{controlname}?view=netframework-4.8
Example for Button:
https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.button?view=netframework-4.8
The actual rendering code is available at:
- GitHub:
https://github.com/microsoft/referencesource - Path:
referencesource/System.Web/UI/WebControls/{ControlName}.cs
Look for these methods in the source:
Render()- Main rendering entry pointRenderContents()- Inner content renderingAddAttributesToRender()- HTML attributes added to the tagRenderBeginTag()/RenderEndTag()- Tag structure
The most reliable method is to create a test page in the samples/BeforeWebForms project:
<%@ Page Language="C#" AutoEventWireup="true" %>
<!DOCTYPE html>
<html>
<head><title>HTML Output Test</title></head>
<body>
<form id="form1" runat="server">
<!-- Test your control here -->
<asp:Button ID="btn1" Text="Click Me" CssClass="my-class" runat="server" />
</form>
</body>
</html>Run the page and view source to capture the exact HTML output.
Web Forms Output:
<input type="submit" name="btn1" value="Click Me" id="btn1" class="my-class" />Key Observations:
- Uses
<input type="submit">NOT<button>element Textproperty becomesvalueattributeIDbecomes bothnameandidattributesCssClassbecomesclassattribute
Web Forms Output:
<span id="lbl1" class="my-class">Label Text</span>Key Observations:
- Renders as
<span>element Textproperty becomes inner contentAssociatedControlIDaddsforattribute (changes to<label>tag)
Web Forms Output:
<a id="link1" class="my-class" href="https://example.com" target="_blank">Link Text</a>Web Forms Output:
<div id="panel1" class="my-class">
<!-- Child content -->
</div>GridView Output:
<table id="grid1" class="my-class" cellspacing="0" rules="all" border="1">
<tr>
<th scope="col">Header1</th>
<th scope="col">Header2</th>
</tr>
<tr>
<td>Data1</td>
<td>Data2</td>
</tr>
</table>DataList Output:
<table id="datalist1" cellspacing="0" border="0">
<tr>
<td><!-- Item content --></td>
</tr>
</table>Repeater Output:
- No wrapper element - renders only the templates
- Most flexible for custom HTML
Web Forms Output:
<span id="val1" class="my-class" style="color:Red;visibility:hidden;">Error message</span>Key Observations:
- Renders as
<span>element ForeColor="Red"becomesstyle="color:Red;"- Initially hidden with
visibility:hiddenordisplay:none ErrorMessagebecomes inner content when invalid
| Web Forms Property | HTML Attribute | Notes |
|---|---|---|
ID |
id, name |
Both attributes set |
CssClass |
class |
Direct mapping |
ToolTip |
title |
Direct mapping |
Enabled="false" |
disabled="disabled" |
Boolean attribute |
TabIndex |
tabindex |
Direct mapping |
AccessKey |
accesskey |
Direct mapping |
BackColor |
style="background-color:X" |
Inline style |
ForeColor |
style="color:X" |
Inline style |
BorderColor |
style="border-color:X" |
Inline style |
BorderWidth |
style="border-width:X" |
Inline style |
BorderStyle |
style="border-style:X" |
Inline style |
Height |
style="height:X" |
Inline style |
Width |
style="width:X" |
Can be attribute or style |
Font-Bold |
style="font-weight:bold" |
Inline style |
Font-Size |
style="font-size:X" |
Inline style |
Visible="false" |
(not rendered) | Element not in DOM |
Use the existing ToStyle() extension method pattern:
// In component .razor file
<button style="@this.ToStyle().Build().NullIfEmpty()">
// ToStyle() builds from IStyle properties:
// - BackColor → background-color
// - ForeColor → color
// - Font properties → font-* styles
// - Height/Width → height/width
// - Border properties → border-* styles@inherits BunitContext
@code {
[Fact]
public void Button_WithCssClass_RendersCorrectHtml()
{
var cut = Render(@<Button Text="Click" CssClass="btn-primary" />);
// Exact match
cut.MarkupMatches(@<input type="submit" value="Click" class="btn-primary" />);
}
[Fact]
public void Button_WithStyles_RendersInlineStyle()
{
var cut = Render(@<Button Text="Click" BackColor="Red" ForeColor="White" />);
var button = cut.Find("input");
button.GetAttribute("style").ShouldContain("background-color");
button.GetAttribute("style").ShouldContain("color");
}
}- Create identical markup in
samples/BeforeWebForms - Capture rendered HTML output
- Create equivalent Blazor component usage in test
- Use
MarkupMatches()or attribute assertions to verify
Web Forms generates ClientID which may differ from ID in nested controls:
<!-- Web Forms with naming container -->
<span id="container1_label1">Text</span>In Blazor, we typically use the simpler @ref pattern and don't replicate naming containers.
ViewState Hidden Field
Web Forms pages include __VIEWSTATE hidden field - this is NOT replicated as Blazor has different state management.
Web Forms includes __EVENTVALIDATION hidden field - NOT replicated in Blazor.
Controls with AutoPostBack="true" add JavaScript onclick/onchange handlers to trigger postback. In Blazor, use proper event binding instead:
<!-- Blazor equivalent of AutoPostBack -->
<DropDownList @bind-SelectedValue="selectedValue" OnSelectedIndexChanged="HandleChange" />- Identify the HTML element type (span, div, input, table, etc.)
- Map all relevant properties to HTML attributes
- Implement style properties using
ToStyle().Build() - Handle
Visible="false"by not rendering - Handle
Enabled="false"withdisabledattribute - Write bUnit tests comparing expected HTML
- Test with existing CSS from Web Forms project
- Document any intentional deviations from Web Forms output