Pimp up your Blazor EditForms with Tailwind CSS!

July 28, 2020 · 8 minute read · Tags: blazor

Blazor’s EditForms are super useful, and I wouldn’t choose to tackle things like model validation in Blazor without them.

But, by design, your vanilla EditForms are bland affairs, simply rendering HTML inputs without style.

Here’s how to rectify that, quickly, with Tailwind CSS.

<EditForm Model="Command" OnValidSubmit="HandleValidSubmit">
    <label for="title">Title</label>
    <InputText id="title" @bind-Value="Command.Title" />
    <label for="slug">Slug</label>
    <InputText id="slug" @bind-Value="Command.Slug" />
    <InputTextArea @bind-Value="Command.Body" />
    <button type="submit">Publish</button>
</EditForm>

Here’s a bog standard EditForm.

And here’s how it looks in a site with Tailwind CSS configured

Um, where’s the form?

Tailwind CSS isn’t Bootstrap.

Where Bootstrap will give you a site which immediately looks like every other Bootstrap site you’ve ever seen, with default styles and decisions made for you, Tailwind CSS takes a different approach.

You get a very minimal set of styles to start with (mainly just to set the defaults for how text appears) then it gets out of your way so you can bend the UI to your will!

This is a little jarring, especially when faced with an almost blank page like this, so where should you start?

Well first up I’d probably try to get these inputs to show up with a border, and move them onto separate ’lines’ by arranging them in div elements.

I’d also add some margins for good measure, using mb which sets a bottom margin in Tailwind.

<EditForm Model="Command" OnValidSubmit="HandleValidSubmit">
    <div class="mb-4">
        <label for="title">Title</label>
        <InputText id="title" @bind-Value="Command.Title" class="border"/>
    </div>
    <div class="mb-4">
        <label for="slug">Slug</label>
        <InputText id="slug" @bind-Value="Command.Slug" class="border"/>
    </div>
    <div class="mb-6">
        <InputTextArea @bind-Value="Command.Body" class="border"/>
    </div>
    <div>
        <button type="submit">Publish</button>
    </div>
</EditForm>

Which renders:

OK that’s beginning to take shape; now I’d work on that button.

Unleash Blazor's Potential

Blazor promises to make it much easier (and faster) to build modern, responsive web applications, using the tools you already know and understand.

Subscribe to my Practical ASP.NET Blazor newsletter and get instant access to the vault.

In there you'll find step-by-step tutorials, source code and videos to help you get up and running using Blazor's component model.

I respect your email privacy. Unsubscribe with one click.

    One of the things I really like about using Tailwind is how quickly you can iterate towards something which looks decent, even if you (like me) wouldn’t consider design, and CSS, to be your strong suits.

    After a bit of experimenting with the button I got to this…

    <button type="submit" 
            class="bg-blue-500 text-white rounded shadow-md p-2">
        Publish
    </button>
    

    Not too shabby!

    Set those widths

    Now our form is taking shape but we probably want to standardise the widths of our inputs.

    We can make the inputs fill the width of their containing element, using w-full.

    <InputText id="title" @bind-Value="Command.Title" class="border w-full"/>
    

    Adding this to all the inputs gives us a much better looking form, except we have a new problem; full in this case really does mean full!

    This starts to look a little odd on anything bigger than a phone screen.

    Aside: I also added a label to the Body input in this last iteration as it looked a little odd sitting there by itself with no label!

    The easiest way to bring those widths under control is to set a max width on the parent element, in this case our EditForm element.

    Here’s where we’re up to…

    <EditForm Model="Command" OnValidSubmit="HandleValidSubmit" class="max-w-lg">
        <div class="mb-4">
            <label for="title">Title</label>
            <InputText id="title" @bind-Value="Command.Title" class="border w-full"/>
        </div>
        <div class="mb-4">
            <label for="slug">Slug</label>
            <InputText id="slug" @bind-Value="Command.Slug" class="border w-full"/>
        </div>
        <div class="mb-6">
            <label for="slug">Body</label>
            <InputTextArea @bind-Value="Command.Body" class="border w-full"/>
        </div>
        <div>
            <button type="submit"
                    class="bg-blue-500 hover:bg-blue-700 text-white rounded shadow-md p-2">
                Publish
            </button>
        </div>
    </EditForm>
    

    Now we just need a few more tweaks to wrap this up.

    When we start actually interacting with the form it’s clear we’re missing some padding for each input.

    We could also do with some space between the labels and the inputs.

    Let’s tackle both:

    <EditForm Model="Command" OnValidSubmit="HandleValidSubmit" class="max-w-lg">
        <div class="mb-4">
            <label for="title" class="mb-2 block font-semibold text-gray-700">Title</label>
            <InputText id="title" @bind-Value="Command.Title" class="border w-full p-3"/>
        </div>
        <div class="mb-4">
            <label for="slug" class="mb-2 block font-semibold text-gray-700">Slug</label>
            <InputText id="slug" @bind-Value="Command.Slug" class="border w-full p-3"/>
        </div>
        <div class="mb-6">
            <label for="slug" class="mb-2 block font-semibold text-gray-700">Body</label>
            <InputTextArea @bind-Value="Command.Body" class="border w-full p-3"/>
        </div>
        <div class="">
            <button type="submit"
                    class="bg-blue-500 hover:bg-blue-700 text-white rounded shadow-md p-2">
                Publish
            </button>
        </div>
    </EditForm>
    

    I’ve made a few small tweaks:

    • Added some style to the labels (including a bottom margin and setting them to display as block elements)
    • Added padding to the InputText and InputTextArea elements

    And here’s how it looks in the browser.

    I’d say that’s a passable first attempt at our form!

    The elephant in the room?

    Ok ok I know what you’re thinking.

    That’s great, I can see how quick it is to iterate and get something which works well on-screen, but are we supposed to just ignore all those repeated classes?

    If we take a look at our form with an eye for repeated styles, it’s pretty clear we have a few elements which are always the same whenever we use them.

    Namely label, InputText, and arguably InputTextArea and button (if we go on to use these same elements again); chances are we want these to appear consistent every time we use them.

    So what should we do?

    Well, the shift in thinking from say something like Razor Pages or MVC to Blazor, is that now we need to think in terms of components.

    We can very easily create re-usable building blocks for our applications, style them up, then use them throughout our application.

    To prove the point, let’s have a go with the label element.

    If we create a new InputLabel.razor file we can copy the markup from on of our labels into it…

    InputLabel.razor

    <label for="title" class="mb-2 block font-semibold text-gray-700">Title</label>
    

    Now to make this re-usable we need to figure out which parts of this will change by usage (vary every time we use it).

    It seems pretty clear that the for attribute value and contents of the label itself will change, but everything else should remain the same.

    To that end we now have two options.

    Option A: Explicit parameters

    Probably the simplest option (especially for a first pass at making this work) would be to create a couple of Parameters to use in place of the currently hardcoded values.

    InputLabel.razor

    <label for="@For" class="mb-2 block font-semibold text-gray-700">@Text</label>
    
    @code
    {
        [Parameter]
        public string For { get; set; }
        
        [Parameter]
        public string Text { get; set; }
    }
    

    Now we can swap out all the labels in our EditForm like so:

    <div class="mb-4">
        <InputLabel For="title" Text="Title"/>
        <InputText id="title" @bind-Value="Command.Title" class="border w-full p-3"/>
    </div>
    

    Option B: Use ChildContent

    Option A works but it makes our InputLabel work slightly differently to the regular label element.

    With ours, you have to provide the label via the Text parameter, but with a real label you put the value of the label inside the element’s HTML selectors.

    Our InputLabel example

    <InputLabel For="title" Text="Title"/>
    

    HTML label

    <label for="title">Title</label>
    

    It might be nice to match the way labels usually work, not least because this would make it easier to drop an InputLabel in as a direct replacement for a regular label.

    We can make this work using a ChildContent Render Fragment.

    InputLabel.razor

    <label for="@For" class="mb-2 block font-semibold text-gray-700">
        @ChildContent
    </label>
    
    @code
    {
        [Parameter]
        public string For { get; set; }
        
        [Parameter]
        public RenderFragment ChildContent { get; set; }
    }
    

    Here’s how we can use this version…

    <InputLabel for="title">Title</InputLabel>
    

    Whichever of these two options you go for, the big benefit now is you can head over to InputLabel.razor whenever you want to adjust your label styles; any changes there will ripple through every form where you’ve used the InputLabel component.

    From here we can go on to do exactly the same for InputText, InputArea, button and any other elements we want to re-use (with consistent styles) throughout our application.

    Iterate and refactor to components

    So next time you’re faced with styling an EditForm, consider using Tailwind CSS.

    Start by rapidly iterating to a design you’re happy with, using Tailwind to style individual elements.

    Once you’re happy with how it looks, consider refactoring the individual, repeated form elements into their own components. Now you have consistent UX ‘building blocks’ which you can use for any form in your application.

    Join the Practical ASP.NET Newsletter

    Ship better Blazor apps, faster. One practical tip every Tuesday.

    I respect your email privacy. Unsubscribe with one click.

      Next up

      Preventing double clicks in Blazor components
      Guard against duplicate events in your Blazor app
      Avoiding interactivity with Blazor?
      Sometimes a little HTML and CSS is all you need
      How to upload a file with Blazor SSR in .NET 8?
      How to handle file uploads without using an interactive render mode?