All skills
wshaddix avatar

/bootstrap5-ui

@5724d98

Production-grade Bootstrap 5.3 patterns for building modern, responsive web UIs in HTML and ASP.NET Core Razor Pages/Views. Use when creating or styling web pages, layouts, navigation, forms, cards, modals, tables, or any UI component with Bootstrap 5. Covers the grid system, responsive breakpoints, utility classes, color modes (dark/light), accessibility, and integration with ASP.NET Core tag helpers and Razor syntax. Trigger on any task involving Bootstrap CSS classes, responsive HTML layouts, Razor Page UI design, or front-end styling for .NET web applications.

Use this Skill: https://skilld.dev/gh/wshaddix/dotnet-skills/bootstrap5-ui

This session only. Nothing lands on disk.

referencesrazor-integration.md

≈6.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Bootstrap 5 + ASP.NET Core Razor Integration

Table of Contents

Layout Structure

_Layout.cshtml with Bootstrap 5

@using Microsoft.AspNetCore.Identity
<!doctype html>
<html lang="en" data-bs-theme="light">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>@ViewData["Title"] - MyApp</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    @await RenderSectionAsync("Styles", required: false)
</head>
<body>
    <header>
        <partial name="_NavbarPartial" />
    </header>

    <div class="container py-4">
        <main role="main">
            <partial name="_StatusMessagePartial" />
            @RenderBody()
        </main>
    </div>

    <footer class="container border-top py-3 mt-auto">
        <div class="row">
            <div class="col-md-6 text-body-secondary">&copy; @DateTime.Now.Year - MyApp</div>
            <div class="col-md-6 text-end">
                <a asp-page="/Privacy" class="text-body-secondary">Privacy</a>
            </div>
        </div>
    </footer>

    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

_ViewImports.cshtml

@using MyApp
@using MyApp.Pages
@namespace MyApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Tag Helpers with Bootstrap

Form Controls

<!-- Standard input with tag helpers -->
<div class="mb-3">
    <label asp-for="Input.Email" class="form-label"></label>
    <input asp-for="Input.Email" class="form-control" />
    <span asp-validation-for="Input.Email" class="text-danger"></span>
</div>

<!-- Select dropdown -->
<div class="mb-3">
    <label asp-for="Input.Category" class="form-label"></label>
    <select asp-for="Input.Category" asp-items="Model.Categories" class="form-select">
        <option value="">Choose...</option>
    </select>
    <span asp-validation-for="Input.Category" class="text-danger"></span>
</div>

<!-- Textarea -->
<div class="mb-3">
    <label asp-for="Input.Description" class="form-label"></label>
    <textarea asp-for="Input.Description" class="form-control" rows="4"></textarea>
    <span asp-validation-for="Input.Description" class="text-danger"></span>
</div>

<!-- Checkbox -->
<div class="mb-3 form-check">
    <input asp-for="Input.AgreeToTerms" class="form-check-input" />
    <label asp-for="Input.AgreeToTerms" class="form-check-label"></label>
    <span asp-validation-for="Input.AgreeToTerms" class="text-danger d-block"></span>
</div>

<!-- Radio buttons from enum -->
@foreach (var status in Enum.GetValues<OrderStatus>())
{
    <div class="form-check">
        <input class="form-check-input" type="radio"
               asp-for="Input.Status" value="@status" id="status-@status" />
        <label class="form-check-label" for="status-@status">@status</label>
    </div>
}

Floating Labels with Tag Helpers

<div class="form-floating mb-3">
    <input asp-for="Input.Email" class="form-control" placeholder="name@example.com" />
    <label asp-for="Input.Email"></label>
    <span asp-validation-for="Input.Email" class="text-danger"></span>
</div>

Form Validation Patterns

Server-Side + Bootstrap Validation Styles

PageModel:

public class CreateModel : PageModel
{
    [BindProperty]
    public InputModel Input { get; set; } = new();

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
            return Page();

        // process...
        return RedirectToPage("./Index");
    }

    public class InputModel
    {
        [Required(ErrorMessage = "Name is required")]
        [StringLength(100, MinimumLength = 2)]
        [Display(Name = "Full Name")]
        public string Name { get; set; } = string.Empty;

        [Required]
        [EmailAddress]
        [Display(Name = "Email Address")]
        public string Email { get; set; } = string.Empty;

        [Required]
        [Range(1, 1000, ErrorMessage = "Amount must be between 1 and 1000")]
        public decimal Amount { get; set; }
    }
}

Razor view with Bootstrap validation classes:

<form method="post" novalidate>
    <div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>

    <div class="mb-3">
        <label asp-for="Input.Name" class="form-label"></label>
        <input asp-for="Input.Name"
               class="form-control @(ViewData.ModelState["Input.Name"]?.Errors.Count > 0 ? "is-invalid" : "")" />
        <span asp-validation-for="Input.Name" class="invalid-feedback"></span>
    </div>

    <div class="mb-3">
        <label asp-for="Input.Email" class="form-label"></label>
        <input asp-for="Input.Email"
               class="form-control @(ViewData.ModelState["Input.Email"]?.Errors.Count > 0 ? "is-invalid" : "")" />
        <span asp-validation-for="Input.Email" class="invalid-feedback"></span>
    </div>

    <button type="submit" class="btn btn-primary">
        <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
        Submit
    </button>
</form>

jQuery Validation Unobtrusive Integration

Add to @section Scripts to apply Bootstrap classes automatically:

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
    <script>
        // Override jQuery Validation to add Bootstrap classes
        const settings = $.data($('form')[0], 'validator')?.settings;
        if (settings) {
            settings.errorClass = 'is-invalid';
            settings.validClass = 'is-valid';
            settings.errorPlacement = function(error, element) {
                error.addClass('invalid-feedback');
                if (element.parent('.form-check').length) {
                    error.insertAfter(element.parent());
                } else if (element.parent('.input-group').length) {
                    error.insertAfter(element.parent());
                } else {
                    error.insertAfter(element);
                }
            };
            settings.highlight = function(element) {
                $(element).addClass('is-invalid').removeClass('is-valid');
            };
            settings.unhighlight = function(element) {
                $(element).removeClass('is-invalid').addClass('is-valid');
            };
        }
    </script>
}

Navigation with Tag Helpers

Navbar with Razor Tag Helpers

<nav class="navbar navbar-expand-lg bg-body-tertiary sticky-top">
    <div class="container">
        <a class="navbar-brand" asp-page="/Index">
            <img src="~/images/logo.svg" alt="MyApp" width="30" height="24"
                 class="d-inline-block align-text-top">
            MyApp
        </a>
        <button class="navbar-toggler" type="button"
                data-bs-toggle="collapse" data-bs-target="#mainNav"
                aria-controls="mainNav" aria-expanded="false"
                aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="mainNav">
            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                <li class="nav-item">
                    <a class="nav-link @(ViewContext.RouteData.Values["page"]?.ToString() == "/Index" ? "active" : "")"
                       asp-page="/Index"
                       aria-current="@(ViewContext.RouteData.Values["page"]?.ToString() == "/Index" ? "page" : null)">
                        Home
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link @(ViewContext.RouteData.Values["page"]?.ToString()?.StartsWith("/Products") == true ? "active" : "")"
                       asp-page="/Products/Index">Products</a>
                </li>
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" href="#" role="button"
                       data-bs-toggle="dropdown" aria-expanded="false">Admin</a>
                    <ul class="dropdown-menu">
                        <li><a class="dropdown-item" asp-page="/Admin/Users">Users</a></li>
                        <li><a class="dropdown-item" asp-page="/Admin/Settings">Settings</a></li>
                        <li><hr class="dropdown-divider"></li>
                        <li><a class="dropdown-item" asp-page="/Admin/Logs">Logs</a></li>
                    </ul>
                </li>
            </ul>

            <!-- Auth-aware section -->
            @if (User.Identity?.IsAuthenticated == true)
            {
                <ul class="navbar-nav">
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" role="button"
                           data-bs-toggle="dropdown">@User.Identity.Name</a>
                        <ul class="dropdown-menu dropdown-menu-end">
                            <li><a class="dropdown-item" asp-page="/Account/Profile">Profile</a></li>
                            <li><hr class="dropdown-divider"></li>
                            <li>
                                <form method="post" asp-page="/Account/Logout">
                                    <button type="submit" class="dropdown-item">Sign out</button>
                                </form>
                            </li>
                        </ul>
                    </li>
                </ul>
            }
            else
            {
                <a class="btn btn-outline-primary me-2" asp-page="/Account/Login">Sign in</a>
                <a class="btn btn-primary" asp-page="/Account/Register">Sign up</a>
            }
        </div>
    </div>
</nav>

Breadcrumbs

<nav aria-label="breadcrumb">
    <ol class="breadcrumb">
        <li class="breadcrumb-item"><a asp-page="/Index">Home</a></li>
        <li class="breadcrumb-item"><a asp-page="/Products/Index">Products</a></li>
        <li class="breadcrumb-item active" aria-current="page">@Model.Product.Name</li>
    </ol>
</nav>

Pagination

@if (Model.TotalPages > 1)
{
    <nav aria-label="Page navigation">
        <ul class="pagination justify-content-center">
            <li class="page-item @(Model.CurrentPage == 1 ? "disabled" : "")">
                <a class="page-link" asp-page="./Index" asp-route-page="@(Model.CurrentPage - 1)">Previous</a>
            </li>
            @for (var i = 1; i <= Model.TotalPages; i++)
            {
                <li class="page-item @(i == Model.CurrentPage ? "active" : "")">
                    <a class="page-link" asp-page="./Index" asp-route-page="@i">@i</a>
                </li>
            }
            <li class="page-item @(Model.CurrentPage == Model.TotalPages ? "disabled" : "")">
                <a class="page-link" asp-page="./Index" asp-route-page="@(Model.CurrentPage + 1)">Next</a>
            </li>
        </ul>
    </nav>
}

Partial Views and Components

Status Message Partial (_StatusMessagePartial.cshtml)

@{
    var statusMessage = TempData["StatusMessage"]?.ToString();
}
@if (!string.IsNullOrEmpty(statusMessage))
{
    var isError = statusMessage.StartsWith("Error");
    <div class="alert @(isError ? "alert-danger" : "alert-success") alert-dismissible fade show" role="alert">
        @statusMessage
        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
    </div>
}

Confirmation Modal Partial

<!-- _DeleteConfirmPartial.cshtml -->
@model (string ItemName, string DeleteUrl)
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="deleteModalLabel">Confirm Delete</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <p>Are you sure you want to delete <strong>@Model.ItemName</strong>?</p>
                <p class="text-danger mb-0">This action cannot be undone.</p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                <form method="post" asp-page-handler="Delete">
                    <button type="submit" class="btn btn-danger">Delete</button>
                </form>
            </div>
        </div>
    </div>
</div>

Usage:

<button type="button" class="btn btn-outline-danger btn-sm"
        data-bs-toggle="modal" data-bs-target="#deleteModal">
    Delete
</button>
<partial name="_DeleteConfirmPartial" model='("Product X", "/Products/Delete")' />

Loading Spinner Partial

<!-- _LoadingPartial.cshtml -->
<div class="d-flex justify-content-center py-5" id="loading-spinner">
    <div class="spinner-border text-primary" role="status">
        <span class="visually-hidden">Loading...</span>
    </div>
</div>

Dark Mode Toggle in Razor

Toggle Button in Navbar

<li class="nav-item dropdown">
    <button class="btn btn-link nav-link dropdown-toggle" data-bs-toggle="dropdown"
            aria-expanded="false" id="bd-theme">
        <span id="bd-theme-text">Theme</span>
    </button>
    <ul class="dropdown-menu dropdown-menu-end">
        <li><button type="button" class="dropdown-item" data-bs-theme-value="light">Light</button></li>
        <li><button type="button" class="dropdown-item" data-bs-theme-value="dark">Dark</button></li>
        <li><button type="button" class="dropdown-item" data-bs-theme-value="auto">Auto</button></li>
    </ul>
</li>

Theme Toggle Script

Place at top of <body> to prevent flash:

<script>
(() => {
    'use strict';
    const getStoredTheme = () => localStorage.getItem('theme');
    const setStoredTheme = theme => localStorage.setItem('theme', theme);
    const getPreferredTheme = () => {
        const stored = getStoredTheme();
        if (stored) return stored;
        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    };
    const setTheme = theme => {
        if (theme === 'auto') {
            document.documentElement.setAttribute('data-bs-theme',
                window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
        } else {
            document.documentElement.setAttribute('data-bs-theme', theme);
        }
    };
    setTheme(getPreferredTheme());
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
        const stored = getStoredTheme();
        if (stored !== 'light' && stored !== 'dark') setTheme(getPreferredTheme());
    });
    window.addEventListener('DOMContentLoaded', () => {
        document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => {
            toggle.addEventListener('click', () => {
                const theme = toggle.getAttribute('data-bs-theme-value');
                setStoredTheme(theme);
                setTheme(theme);
            });
        });
    });
})();
</script>

Security Considerations

Anti-Forgery Tokens

ASP.NET Core auto-includes anti-forgery tokens in <form method="post"> tag helper forms. For AJAX:

@Html.AntiForgeryToken()
<script>
    const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
    fetch('/api/data', {
        method: 'POST',
        headers: {
            'RequestVerificationToken': token,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(data)
    });
</script>

Content Security Policy (CSP) with Bootstrap

When using CSP headers, Bootstrap's inline styles require nonce or unsafe-inline. Prefer nonce:

// In middleware or _Layout.cshtml
var nonce = Guid.NewGuid().ToString("N");
Context.Items["CspNonce"] = nonce;
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" nonce="@Context.Items["CspNonce"]" />

Sanitize User Content

Razor auto-encodes output with @. For raw HTML (rare), use @Html.Raw() only with sanitized content. Never render untrusted input as raw HTML in Bootstrap components like toasts or modals.

Complete Page Examples

CRUD List Page

@page
@model Products.IndexModel
@{ ViewData["Title"] = "Products"; }

<div class="d-flex justify-content-between align-items-center mb-4">
    <h1 class="h3 mb-0">Products</h1>
    <a asp-page="./Create" class="btn btn-primary">
        <i class="bi bi-plus-lg"></i> Add Product
    </a>
</div>

<!-- Search & Filter Bar -->
<div class="card mb-4">
    <div class="card-body">
        <form method="get" class="row g-3">
            <div class="col-md-4">
                <div class="input-group">
                    <input type="text" class="form-control" name="search"
                           value="@Model.SearchTerm" placeholder="Search products...">
                    <button class="btn btn-outline-secondary" type="submit">Search</button>
                </div>
            </div>
            <div class="col-md-3">
                <select name="category" class="form-select" onchange="this.form.submit()">
                    <option value="">All Categories</option>
                    @foreach (var cat in Model.Categories)
                    {
                        <option value="@cat.Id" selected="@(cat.Id == Model.SelectedCategory)">@cat.Name</option>
                    }
                </select>
            </div>
            <div class="col-md-3">
                <select name="sort" class="form-select" onchange="this.form.submit()">
                    <option value="name">Name A-Z</option>
                    <option value="name_desc">Name Z-A</option>
                    <option value="price">Price Low-High</option>
                    <option value="price_desc">Price High-Low</option>
                </select>
            </div>
        </form>
    </div>
</div>

<!-- Results Table -->
<div class="table-responsive">
    <table class="table table-striped table-hover align-middle">
        <thead class="table-dark">
            <tr>
                <th>Name</th>
                <th>Category</th>
                <th class="text-end">Price</th>
                <th class="text-center">Status</th>
                <th class="text-end">Actions</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model.Products)
            {
                <tr>
                    <td>
                        <a asp-page="./Details" asp-route-id="@item.Id" class="text-decoration-none fw-semibold">
                            @item.Name
                        </a>
                    </td>
                    <td>@item.Category</td>
                    <td class="text-end">@item.Price.ToString("C")</td>
                    <td class="text-center">
                        <span class="badge @(item.IsActive ? "text-bg-success" : "text-bg-secondary")">
                            @(item.IsActive ? "Active" : "Inactive")
                        </span>
                    </td>
                    <td class="text-end">
                        <div class="btn-group btn-group-sm">
                            <a asp-page="./Edit" asp-route-id="@item.Id" class="btn btn-outline-primary">Edit</a>
                            <button type="button" class="btn btn-outline-danger"
                                    data-bs-toggle="modal" data-bs-target="#deleteModal-@item.Id">
                                Delete
                            </button>
                        </div>
                    </td>
                </tr>
            }
        </tbody>
    </table>
</div>

@if (!Model.Products.Any())
{
    <div class="text-center py-5 text-body-secondary">
        <p class="fs-5">No products found.</p>
        <a asp-page="./Create" class="btn btn-primary">Add your first product</a>
    </div>
}

Create/Edit Form Page

@page
@model Products.CreateModel
@{ ViewData["Title"] = "Create Product"; }

<nav aria-label="breadcrumb">
    <ol class="breadcrumb">
        <li class="breadcrumb-item"><a asp-page="/Index">Home</a></li>
        <li class="breadcrumb-item"><a asp-page="./Index">Products</a></li>
        <li class="breadcrumb-item active" aria-current="page">Create</li>
    </ol>
</nav>

<div class="row justify-content-center">
    <div class="col-lg-8">
        <div class="card shadow-sm">
            <div class="card-header">
                <h4 class="card-title mb-0">Create Product</h4>
            </div>
            <div class="card-body">
                <form method="post" enctype="multipart/form-data">
                    <div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>

                    <div class="row mb-3">
                        <div class="col-md-8">
                            <label asp-for="Input.Name" class="form-label"></label>
                            <input asp-for="Input.Name" class="form-control" autofocus />
                            <span asp-validation-for="Input.Name" class="text-danger"></span>
                        </div>
                        <div class="col-md-4">
                            <label asp-for="Input.Price" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text">$</span>
                                <input asp-for="Input.Price" class="form-control" />
                            </div>
                            <span asp-validation-for="Input.Price" class="text-danger"></span>
                        </div>
                    </div>

                    <div class="mb-3">
                        <label asp-for="Input.Description" class="form-label"></label>
                        <textarea asp-for="Input.Description" class="form-control" rows="4"></textarea>
                        <span asp-validation-for="Input.Description" class="text-danger"></span>
                    </div>

                    <div class="row mb-3">
                        <div class="col-md-6">
                            <label asp-for="Input.CategoryId" class="form-label"></label>
                            <select asp-for="Input.CategoryId" asp-items="Model.Categories" class="form-select">
                                <option value="">Select category...</option>
                            </select>
                            <span asp-validation-for="Input.CategoryId" class="text-danger"></span>
                        </div>
                        <div class="col-md-6">
                            <label asp-for="Input.Image" class="form-label"></label>
                            <input asp-for="Input.Image" class="form-control" type="file" accept="image/*" />
                            <span asp-validation-for="Input.Image" class="text-danger"></span>
                        </div>
                    </div>

                    <div class="form-check form-switch mb-4">
                        <input asp-for="Input.IsActive" class="form-check-input" />
                        <label asp-for="Input.IsActive" class="form-check-label"></label>
                    </div>

                    <div class="d-flex gap-2">
                        <button type="submit" class="btn btn-primary">Create Product</button>
                        <a asp-page="./Index" class="btn btn-outline-secondary">Cancel</a>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Source: SKILL.md on GitHub

No alerts14d4 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    The skill provides comprehensive UI patterns for Bootstrap 5 and ASP.NET Core integration. No malicious patterns, obfuscation, or security risks were detected. It follows industry best practices by including subresource integrity (SRI) hashes for external assets and provides explicit security guidance for mitigating cross-site scripting (XSS) and cross-site request forgery (CSRF).

  • Socket14d

    No alerts

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer6mo

    1/3 files flagged

Signed by skilld at 5724d98. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 8 months ago

README badge

README badge for wshaddix/dotnet-skills/bootstrap5-ui