Skip to content

Writing your own UI libraries

Static Components has been optimized for the usage of Razor Class Libraries. In fact, as a proof of concept, and for personal use, I wrote an implementation of DevDojo’s PinesUI by making it native to ASP.NET Core and utilizing all the quality of life features that Static Components allows you.

Razor Class Libraries are useful for creating UI component libraries meant to be reused, this can be a great way to isolate your UI-only components from your application’s presentation components.

Razor Class Libraries can be combined with your CSS library of choice to make UI elements easier to use.

Input components are the one part of a UI kit that needs more than a Razor template, so Static Components ships a set of headless base classes — StaticInput, StaticCheckbox, StaticRadio, StaticSelect, StaticLabel — that handle the binding for you.

Inherit from the one matching the element you’re building, then write your markup:

public class PinesInput : StaticInput { }
@model PinesInput
<input static-for="@Model.InputExpression"
static-attributes="@Model.AdditionalAttributes"
class="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm" />
@* consumer view *@
<pines-input asp-for="Form.Email" class="mt-4" placeholder="you@example.com" />

Two attributes are doing the work here, and it’s worth being clear about why there are two.

asp-for on the consumer-facing tag exists to give your users a familiar interface. Anyone who has written an ASP.NET Core form already knows asp-for, so binding <pines-input asp-for="Form.Email"> should feel identical to binding <input asp-for="Form.Email">. Your component drops into an existing form with no new convention to learn, and it keeps working with the validation and model binding your users already rely on. Exposing it costs you a single ModelExpression property, which the headless base classes already declare.

static-for inside the template is the plumbing that makes that possible. By the time the expression reaches your inner <input> it has been wrapped a second time, and the built-in asp-for helper would read the metadata of ModelExpression itself rather than of the property your consumer targeted — producing a wrong name, id and data-val-* set. static-for reaches through that extra layer. Consumers never see it; it only ever appears inside a headless component’s template.

Headless Components covers the full set of base classes, the matching static-* tag helpers for checkboxes, radios, selects and textareas, and the validation attributes they emit.

This is just a summary of habits you will find useful when writing components that need to be maintained.

When using Razor Pages or MVC it is important to use ASP.NET Core official tag helpers for link generation, or making forms easier to work with, here are some tips that can make it easier to create your own custom link and form input components:

Tag HelperHow to integrateExample
asp-forExpose it on your component as a ModelExpression property so consumers bind exactly as they would to a built-in input. Inside your template, hand that expression to the inner element with static-for rather than asp-for — see Form inputs.PinesInput example
asp-controller, asp-action, asp-pageCan be configured on custom Static Components for link generation.PinesSidebarLink code-behind and view

In general, maintaining ASP.NET Core conventions of usage will make your components better and more consistent for your users.