Install Preline UI with Hugo using Tailwind CSS
Install Preline UI with Tailwind CSS in Hugo projects, including JavaScript plugin setup, Hugo Pipes, static scripts, and optional dependencies.
Installation
Please note that the plugin has been tested with the 0.164.0 (extended) version of the framework. The framework was installed using the standard hugo new site <project-name> command.
This guide uses Hugo's native css.TailwindCSS and js.Build pipes (available since Hugo v0.128.0 and required since v0.161.0, when Hugo dropped support for the standalone Tailwind CSS binary). If you are using your own project structure or a different version, pay attention to the file paths and features of your version!
Hugo quick setup
Start with a working Hugo site, then add Tailwind CSS before importing Preline UI.
Some components rely on third-party libraries. The setup below assumes full Preline UI usage with those dependencies preloaded. If you do not plan to use those components, you can remove the related libraries from your configuration.
-
Install Preline UI
Install
preline. Hugo Pipes (css.TailwindCSS,js.Build) reads packages directly fromnode_modules, so no manual copy intostatic/is needed.Terminalnpm install prelinePreline UI uses the Tailwind CSS Forms plugin across form components. Install it if you have not already:
npm install -D @tailwindcss/forms -
Set up Tailwind CSS
Install Tailwind CSS and its CLI package. Hugo's native
css.TailwindCSSpipe shells out to this CLI. As of Hugo v0.161.0, the standalone Tailwind CSS binary is no longer supported, so the CLI must come from npm.Terminalnpm install tailwindcss @tailwindcss/cli -
Configure Hugo for Tailwind CSS
css.TailwindCSSneeds Hugo's build-stats feature enabled so Tailwind can see which classes your templates actually use (Tailwind can't scan compiled.htmloutput the way it scans plain source files). Add this block tohugo.toml.hugo.toml[build] [build.buildStats] enable = true [[build.cachebusters]] source = 'assets/notwatching/hugo_stats\.json' target = 'css' [[build.cachebusters]] source = '(postcss|tailwind)\.config\.js' target = 'css' [module] [[module.mounts]] source = 'assets' target = 'assets' [[module.mounts]] disableWatch = true source = 'hugo_stats.json' target = 'assets/notwatching/hugo_stats.json' -
Include Preline CSS
Import Preline into
projects_root_directory/assets/css/main.css.main.css@import "tailwindcss"; @import "preline/variants.css"; @source "node_modules/preline/dist/*.js"; @source "hugo_stats.json"; /* Optional Preline UI Datepicker Plugin */ /* @import "preline/datepicker-styles-utility.css"; */ /* Plugins */ /* @plugin "@tailwindcss/forms"; */ /* Preline Themes */ @import "preline/theme.css"; /* Optional Preline UI Datatable Plugin: DataTables.net renders its own search/length/paging controls alongside Preline's own; hide the native ones since they can't be disabled through DataTables options alone. */ /* .dt-layout-row:has(.dt-search), .dt-layout-row:has(.dt-length), .dt-layout-row:has(.dt-paging) { display: none !important; } */@sourcepaths here resolve relative to the project root, not relative tomain.css's own location on disk, which is different from bundler-based setups (Vite, webpack), where@sourceresolves relative to the CSS file itself. Copying a../../node_modules/...-style path from a non-Hugo example will silently match zero files: no error, no warning, and any class that only exists inside Preline's JS bundle (not in your rendered HTML) will quietly never make it into the compiled CSS.See the Theme docs to learn more about Preline Themes.
-
Render the stylesheet with Hugo Pipes
Instead of invoking the Tailwind CLI directly, call it through Hugo's native
css.TailwindCSSfunction from a partial, so the stylesheet is built, minified in production, and fingerprinted automatically. Createlayouts/_partials/css.html.layouts/_partials/css.html{{ with resources.Get "css/main.css" }} {{ $opts := dict "minify" (not hugo.IsDevelopment) "skipInlineImportsNotFound" true }} {{ with . | css.TailwindCSS $opts }} {{ if hugo.IsDevelopment }} <link rel="stylesheet" href="{{ .RelPermalink }}"> {{ else }} {{ with . | fingerprint }} <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"> {{ end }} {{ end }} {{ end }} {{ end }}skipInlineImportsNotFoundis required here: Hugo's own CSS import inliner doesn't understand bare npm-style specifiers like@import "preline/theme.css";and fails the build withfailed to resolve CSS @importif this option is left out. Setting it totrueleaves those imports unresolved for Hugo's own inliner and lets the Tailwind CLI, which does understandnode_modulesresolution, handle them instead. -
Bundle the scripts with Hugo Pipes
Create
assets/js/preline.ts. Optional third-party libraries used by some plugins (Datatable, Datepicker, File Upload, Range Slider) must be assigned towindowbefore thepreline/non-autoimport: Preline'sHSStaticMethodsbuilds its plugin availability map once, when that module is evaluated, so a library that lands onwindowafterwards will not retroactively enable its plugin. Only import the libraries the components you actually use need, and remove the rest.assets/js/preline.tsimport $ from "jquery"; import _ from "lodash"; import Dropzone from "dropzone"; import noUiSlider from "nouislider"; window.$ = $; window.jQuery = $; window._ = _; window.Dropzone = Dropzone; window.noUiSlider = noUiSlider; async function initPreline() { const dtModule = await import("datatables.net-dt"); window.DataTable = dtModule.default ?? dtModule; const vcModule = await import("vanilla-calendar-pro"); window.VanillaCalendarPro = vcModule.Calendar ?? vcModule.default ?? vcModule; const { HSStaticMethods } = await import("preline/non-auto"); HSStaticMethods.autoInit(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initPreline); } else { initPreline(); }Create
layouts/_partials/js.htmlto bundle it with esbuild viajs.Build.layouts/_partials/js.html{{ with resources.Get "js/preline.ts" }} {{ $opts := dict "targetPath" "js/preline.js" "minify" (not hugo.IsDevelopment) }} {{ with . | js.Build $opts }} <script src="{{ .RelPermalink }}" type="module"></script> {{ end }} {{ end }}Call both partials from
layouts/baseof.html. Hugo's template system moved base layouts to the project root in v0.146.0, so uselayouts/baseof.html, notlayouts/_default/baseof.html. Wrap the CSS partial intemplates.Defer: the build-stats content-detection from the previous steps needs that deferred render pass, and calling the partial directly will break class detection.layouts/baseof.htmlDatepicker's own availability check only verifies that
vanilla-calendar-prois present, but it does not check forlodash, even though the plugin uses it internally. Forgettingwindow._will pass that check and then throw at runtime the moment a datepicker actually renders. File Upload's check does verify bothlodashandDropzone.
Specific guide
Continue with a focused guide for wiring Preline UI JavaScript plugins into this framework.
Optional Preline UI styles
Preline UI ships with a small set of opinionated base styles. If you want them in your project, add them to your CSS file. These defaults used to come bundled with Tailwind CSS v3, so they are still available as an optional layer in Preline UI.
/* Adds pointer cursor to buttons */
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
/* Defaults hover styles on all devices */
@custom-variant hover (&:hover);