Update v5.0 - Preline MCP, AI Prompts, Animated Icons and more. Visit Changelog

ASP.NET Core

Using Preline UI with ASP.NET Core

A practical guide to wiring Preline UI into ASP.NET Core across MVC and Razor Pages, including serving Preline from wwwroot with LibMan, the Vite build path, and re-initializing after partial view and AJAX updates.

ASP.NET Core and Preline UI fit together cleanly once you separate the two layers. ASP.NET Core is a cross-platform .NET web framework that renders HTML on the server through Razor, either with MVC controllers and views or with page-centric Razor Pages. Preline UI is the client-side behavior layer that reads that HTML in the browser and wires up the interaction. The Preline UI script your wwwroot folder serves is what carries that behavior across the .NET-to-browser boundary.

This guide walks through the integration choices that matter in a real ASP.NET Core project: where to serve Preline UI from wwwroot with LibMan, how to add a Vite build step when you want one, how to re-initialize after partial views or AJAX swap markup into the page, how to use Preline UI markup inside .cshtml views and pages, and how to avoid stale plugin references as content changes.

Start with the ASP.NET Core mental model

Preline UI is a DOM-driven Tailwind CSS component system. ASP.NET Core produces the markup, and Preline UI attaches behavior to that markup once it exists in the browser. That is why the same dropdown, overlay, tabs, tooltip, or select markup can move between ASP.NET Core, plain HTML, React, Vue, Svelte, Rails, Phoenix LiveView, and similar stacks.

ASP.NET Core supports two server-side UI styles over the same asset pipeline. MVC pairs controllers with Razor views in .cshtml files, while Razor Pages keep the markup and its code-behind together in page-centric .cshtml files. Both render HTML on the server and share the same wwwroot static assets and _Layout.cshtml shared layout, so Preline UI is wired the same way for either one.

The detail that matters here is that ASP.NET Core is a traditional multi-page app. Each navigation is a full HTTP request that returns a fresh document, so a single page-load scan covers the markup and there is no DOM-morph re-init problem like the one in Blazor Server. That changes only when you opt into partial updates, for example jQuery AJAX, the fetch API, or htmx swapping a partial view into the page without a reload. In those cases you re-run initialization after the new markup lands. Everything below is about running autoInit at the right moment for each case.

Serve Preline UI from wwwroot with LibMan

Serving Preline UI from wwwroot is the recommended path for MVC and Razor Pages, because it matches how ASP.NET Core already serves static assets and needs no Node.js bundler. LibMan, the Library Manager built into the .NET tooling, is the simplest way to manage that download. Add a libman.json at the project root that pulls the Preline UI bundle into wwwroot/lib/preline/.

libman.json
                        
                          {
                            "version": "1.0",
                            "defaultProvider": "jsdelivr",
                            "libraries": [
                              {
                                "library": "preline@latest",
                                "destination": "wwwroot/lib/preline/",
                                "files": ["dist/preline.js"]
                              }
                            ]
                          }
                        
                      

Run libman restore to fetch the files, or let Visual Studio restore them on build. Reference the compiled Tailwind CSS and the Preline UI bundle from your shared layout, Views/Shared/_Layout.cshtml for MVC or Pages/Shared/_Layout.cshtml for Razor Pages. The tilde ~/ prefix resolves to wwwroot, and the asp-append-version Tag Helper adds a content hash for cache-busting. The full ASP.NET Core installation covers the Tailwind CSS build step that produces wwwroot/css/tailwind.css with the Preline UI variants.

LibMan only fetches the runtime script, so npm install preline is still required alongside it: your Tailwind CSS source imports node_modules/preline/variants.css and node_modules/preline/theme.css, and @source-scans node_modules/preline/dist/*.js, none of which LibMan provides on its own.

Views/Shared/_Layout.cshtml
                        
                          <!-- In <head> -->
                          <link rel="stylesheet" href="~/css/tailwind.css" asp-append-version="true" />

                          <!-- Before the closing body tag -->
                          <script src="~/lib/preline/dist/preline.js" asp-append-version="true"></script>
                          <script>
                            window.HSStaticMethods.autoInit();
                          </script>
                        
                      

The bundled preline.js exposes the window.HSStaticMethods global and the plugin classes such as window.HSDropdown. Because the script tag sits at the end of the body, the document is parsed by the time it runs, so a single autoInit call scans the page. Every standard ASP.NET Core navigation is a full request, so the layout reloads and that scan runs again on each page. For a quick trial without LibMan, you can drop in the Preline UI CDN script instead, but a managed wwwroot copy is the better fit for a real project.

Add a Vite build step when you want a bundler

LibMan covers most MVC and Razor Pages projects, but you might want a JavaScript bundler when you already write a lot of front-end code or want tree-shaken imports. The Vite.AspNetCore NuGet package wires Vite into the ASP.NET Core build and dev server, or you can run a standalone Vite config that outputs to wwwroot. With a bundler in place, import from preline/non-auto so nothing runs until your code decides the DOM is ready.

Scripts/main.js
                        
                          import "./styles/app.css";
                          import { HSStaticMethods } from "preline/non-auto";

                          function autoInit() {
                            HSStaticMethods.autoInit();
                          }

                          if (document.readyState === "loading") {
                            document.addEventListener("DOMContentLoaded", autoInit);
                          } else {
                            autoInit();
                          }

                          // Keep HSStaticMethods reachable for partial updates
                          window.HSStaticMethods = HSStaticMethods;
                        
                      

Load the bundled entry from your shared layout the same way you would any other script, pointing at the file Vite emits into wwwroot. Attaching HSStaticMethods to window keeps it reachable by name from inline scripts and AJAX callbacks, which matters once partial updates change the DOM. Standard navigation still re-runs the entry on each full request, so autoInit scans the fresh DOM every page.

Reinitialize after partial views and AJAX updates

This is the one step that needs care in ASP.NET Core. A full navigation reloads the layout and re-runs autoInit, so plain MVC and Razor Pages need nothing extra. But when you load a partial view through jQuery AJAX, the fetch API, or htmx and insert the response into the page, that markup arrives after the first scan. The new nodes never get initialized until you re-run autoInit. This is the same re-init problem you see in Blazor Server and Livewire, and the fix is the same shape: run autoInit again after the markup lands.

With jQuery AJAX or the fetch API, call autoInit in the callback after you insert the partial.

JavaScript
                        
                          async function loadPanel() {
                            const res = await fetch("/Home/PanelPartial");
                            document.querySelector("#panel").innerHTML = await res.text();

                            // Initialize Preline UI markup that just arrived
                            window.HSStaticMethods.autoInit();
                          }
                        
                      

If you use htmx to swap partial views, listen for its htmx:afterSwap event once and let it cover every swap. autoInit skips elements that already have plugin instances, so re-running it on every swap is safe and never double-initializes existing markup.

JavaScript
                        
                          // Fires after htmx swaps new markup into the page
                          document.body.addEventListener("htmx:afterSwap", () => {
                            window.HSStaticMethods.autoInit();
                          });
                        
                      

Choose imports by the level of control you need

The preline.js bundle that LibMan downloads exposes window.HSStaticMethods and the plugin classes such as window.HSDropdown as globals. That global surface is what makes the inline autoInit call and the AJAX callbacks above work, because they reach the methods by name without any module system.

You can scope autoInit and cleanCollection to specific plugins by passing their names, which keeps a partial update from scanning the whole document when only one region changed.

JavaScript
                        
                          window.HSStaticMethods.autoInit(["dropdown", "overlay"]);
                          window.HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
                        
                      

If you run a JavaScript bundler such as Vite alongside your ASP.NET Core assets, import from preline/non-auto instead so nothing runs until your code decides the DOM is ready. Attach what your inline scripts and AJAX callbacks need to window so they can still reach it by name.

Scripts/main.js
                        
                          import { HSStaticMethods } from "preline/non-auto";

                          window.HSStaticMethods = HSStaticMethods;
                        
                      

Single plugin packages keep small ASP.NET Core surfaces focused

Not every ASP.NET Core app needs the whole library. Preline UI plugins can also be consumed from single-plugin dependencies when those packages are available in your dependency set, for example @preline/dropdown, @preline/overlay, @preline/select, or @preline/range-slider. This keeps the integration focused when only a small part of the page needs Preline UI behavior.

Terminal
                        
                          npm install @preline/dropdown
                        
                      

Under a Vite build, import the single plugin's class from its /non-auto entry and run it once the DOM is ready. The single-package auto entry is import "@preline/dropdown"; use it only for static pages with full reloads, and reach for the /non-auto entry once partial updates control the timing.

Scripts/main.js
                        
                          import HSDropdown from "@preline/dropdown/non-auto";

                          document.addEventListener("DOMContentLoaded", () => {
                            HSDropdown.autoInit();
                          });
                        
                      

Use Preline UI markup in Razor views and pages

Razor views and pages only emit HTML, so Preline UI markup drops straight into a .cshtml file with the same Tailwind CSS classes you would use anywhere else. ASP.NET Core uses the standard class attribute, so the markup is identical to the plain HTML examples. The bundle your layout loads is what attaches the behavior once the page reaches the browser.

Views/Shared/_ActionsMenu.cshtml
                        
                          <div class="hs-dropdown relative inline-flex">
                            <button id="hs-dropdown-example" type="button" class="hs-dropdown-toggle ..." aria-haspopup="menu" aria-expanded="false" aria-label="Dropdown">
                              Actions
                            </button>

                            <div class="hs-dropdown-menu ... hidden" role="menu" aria-orientation="vertical" aria-labelledby="hs-dropdown-example">
                              ...
                            </div>
                          </div>
                        
                      

ASP.NET Core Tag Helpers such as asp-controller, asp-action, and asp-page run on the server and render plain anchors and form attributes, so they sit alongside Preline UI classes without affecting client-side behavior. The one thing to watch in .cshtml is Razor's @ syntax: a single @ in markup starts a Razor expression, so escape a literal at-sign in your HTML as @@. Preline UI relies on data-* attributes and classes rather than @, so this only comes up when a value of your own contains an @ sign.

Markup you repeat across pages, or that carries real per-instance data, is a better fit for a Razor partial view with a typed model than for copy-pasted blocks. Keep the data in the page's PageModel (or the MVC action's view model), not hardcoded in the view.

Models/AccordionItem.cs
                        
                          namespace YourApp.Models;

                          public record AccordionItem(string Title, string Content);
                        
                      
Pages/Shared/_AccordionItem.cshtml
                        
                      

Render the partial once per item, passing the model explicitly.

Pages/Accordion.cshtml
                        
                      

Some components take their configuration as JSON through a data-hs-* attribute, for example Datatable's data-hs-datatable or Datepicker's data-hs-datepicker. Build that JSON on the server with System.Text.Json.JsonSerializer.Serialize and assign it to a local variable instead of hand-writing a JSON string in the markup. Razor HTML-encodes the value when you print it, so the quotes inside the attribute stay valid without any manual escaping.

Razor
                        
                      

Make sure your shared layout loads the script that ships Preline UI so every page that renders Preline UI markup also ships the script that initializes it.

Use manual instances when you own a specific node

Page-level autoInit is good for most markup. A manual instance is better when one specific node needs explicit control, for example a menu you re-render through AJAX. Create the instance after the markup exists and guard it with getInstance so a re-run does not double-initialize the same node.

JavaScript
                        
                          function initUserMenu() {
                            const el = document.querySelector("#user-menu");
                            if (el && !window.HSDropdown.getInstance(el)) {
                              new window.HSDropdown(el);
                            }
                          }
                        
                      

The getInstance guard keeps a single instance per node across updates. If a partial swap is allowed to replace the node, prefer page-level autoInit over a long-lived manual instance, since the element the reference points at can be swapped out.

Cleanup matters because Preline UI keeps registries

Preline UI stores plugin instances in internal collections such as window.$hsDropdownCollection. That registry lets plugins coordinate in plain HTML, ASP.NET Core, and other environments without framework context.

A full navigation reloads the page and resets the registry, so plain MVC and Razor Pages need nothing extra. Partial view and AJAX updates replace markup without a reload, so a registry entry can linger as a stale reference when its node is swapped out. When you intentionally remove a region of Preline UI markup, clean the relevant collection before re-running autoInit so the registry only holds live nodes.

Cleanup
                        
                          window.HSStaticMethods.cleanCollection("dropdown");
                          window.HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
                        
                      

Optional dependencies only matter for the plugins that use them

Most core plugins do not use jQuery. Dropdowns, overlays, tooltips, popovers, tabs, and similar components use plain JavaScript. Positioning behavior uses @floating-ui/dom.

jQuery is only relevant for Datatable because datatables.net depends on it. If jQuery and DataTables are not present, Datatable should not initialize. That dependency does not affect dropdowns, overlays, tabs, or tooltips.

Range Slider uses the JavaScript API from noUiSlider. Preline UI remains responsible for the Tailwind CSS markup and behavior wrapper, so you do not need to bring in noUiSlider CSS just to make Preline UI styling work.

File Upload depends on lodash and dropzone, and Datepicker depends on lodash. Load both before the Preline UI script, scoped to the view that uses the component rather than the shared layout. Datepicker also needs its Tailwind CSS grid utility classes: import the public preline/datepicker-styles-utility.css export alongside variants.css in your Tailwind source. It is separate from the aggregate variants.css import.

"Scoped to the view" needs one more adjustment in _Layout.cshtml. The default MVC and Razor Pages project templates render @RenderSectionAsync("Scripts") after the layout's own scripts, right before </body>. If the Preline UI script tag sits above that section in the default order, a page's @section Scripts block loads after Preline UI has already run its startup scan, so the optional dependency is not there yet and the component quietly fails to initialize. Move the section above the Preline UI script tag instead, so page-specific dependencies are always in place first.

Pages/Shared/_Layout.cshtml
                        
                      
Pages/Datepicker.cshtml
                        
                      

The practical checklist

  • Serve Preline UI from wwwroot with LibMan into wwwroot/lib/preline/, and reference it from _Layout.cshtml with the ~/ path and asp-append-version Tag Helper; the bundle exposes window.HSStaticMethods.
  • Standard MVC and Razor Pages navigation is a full request, so a single window.HSStaticMethods.autoInit() at the end of the layout covers each page.
  • After a partial view or AJAX update, re-run autoInit in the callback, or once on htmx:afterSwap when you use htmx.
  • Add a Vite build only when you want a bundler; import from preline/non-auto and attach HSStaticMethods to window for your partial updates.
  • Use the standard class attribute in .cshtml markup, and escape a literal at-sign in your own values as @@ so Razor renders a single @.
  • Guard manual instances with getInstance, and call cleanCollection when you intentionally remove a region of initialized markup.
  • Install optional third-party dependencies only for the plugins that need them, such as datatables.net for Datatable or noUiSlider for Range Slider.

© 2026 Preline Labs.