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

Install Preline UI with Phoenix LiveView using Tailwind CSS

Install Preline UI with Tailwind CSS in Phoenix LiveView projects, including JavaScript plugin setup, LiveView hooks, app scripts, and optional dependencies.

Installation

Please note that the plugin has been tested with Phoenix 1.8.9 (Elixir 1.17.3, OTP 27). The framework was installed using the standard mix phx.new <project-name> command.
If you are using your own project structure or a different version, pay attention to the file paths and features of your version!

Phoenix LiveView quick setup

If Tailwind CSS is not set up yet, start with the official Phoenix + Tailwind CSS guide first.

  1. Install Preline UI

    Install preline with your preferred package manager inside the assets directory.

    Terminal
                              
                                cd assets
                                npm install preline
                              
                            

    Preline UI uses the Tailwind CSS Forms plugin across form components. Install it if you have not already: npm install -D @tailwindcss/forms

  2. Import Preline CSS and source files

    Import variants.css into app.css after the tailwindcss import, then add the @source entry for Preline UI JavaScript and one for your own app's templates and components.

    app.css
                              
                                @import "tailwindcss";
    
                                @import "../node_modules/preline/variants.css";
                                @source "../node_modules/preline/dist/*.js";
                                /* Your app's own templates and components */
                                @source "../../lib/my_app_web";
    
                                /* Optional Preline UI Datepicker Plugin */
                                /* @import "../node_modules/preline/datepicker-styles-utility.css"; */
    
                                /* Plugins */
                                /* @plugin "@tailwindcss/forms"; */
    
                                /* Preline Themes */
                                @import "../node_modules/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;
                                }
                                */
                              
                            

    That last @source is not optional. Tailwind's automatic content detection scans downward from the CSS file's own directory, so it never reaches lib/, which is a sibling of assets/, not a descendant of it. Without an explicit @source pointed at your app's web directory, none of the Tailwind classes in your .html.heex templates or function components will compile, only classes that happen to appear in assets/ itself or in Preline UI's own JavaScript. This applies to any Phoenix project, not just ones using Preline UI.

    See the Theme docs to learn more about Preline Themes.

  3. Reinitialize Preline UI with a LiveView Hook

    LiveView patches the DOM in place instead of reloading the page, so a plugin has to be initialized again wherever it gets mounted or patched. A Phoenix LiveView Hook is the mechanism LiveView itself documents for this: define one hook in app.js that re-runs autoInit, then attach it with phx-hook to whichever elements render Preline UI markup.

    app.js
                              
                                // Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
                                import "phoenix_html"
    
                                // Establish Phoenix Socket and LiveView configuration.
                                import { Socket } from "phoenix"
                                import { LiveSocket } from "phoenix_live_view"
                                import { hooks as colocatedHooks } from "phoenix-colocated/my_app"
                                import topbar from "../vendor/topbar"
    
                                // mounted() runs when the element first enters the DOM, updated()
                                // runs after LiveView patches it - together they cover both the
                                // first render and every re-render after.
                                const Hooks = {
                                  PrelineInit: {
                                    mounted() { window.HSStaticMethods?.autoInit() },
                                    updated() { window.HSStaticMethods?.autoInit() },
                                  },
                                }
    
                                const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
                                const liveSocket = new LiveSocket("/live", Socket, {
                                  params: { _csrf_token: csrfToken },
                                  hooks: { ...colocatedHooks, ...Hooks }
                                })
    
                                // Show progress bar on live navigation and form submits
                                topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
                                window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
                                window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
    
                                // Connect if there are any LiveViews on the page
                                liveSocket.connect()
    
                                // Preline UI. Dynamic import, not "import 'preline'" at the top of
                                // the file: static imports are hoisted, so if you later add a plugin
                                // that depends on a third-party library (Datatable, Datepicker, Range
                                // Slider, File Upload), a same-file window.X assignment placed above a
                                // static "preline" import would not reliably run before it. Importing
                                // it dynamically here avoids that trap from the start.
                                if (document.readyState === "loading") {
                                  document.addEventListener("DOMContentLoaded", () => import("preline"))
                                } else {
                                  import("preline")
                                }
                              
                            

    Then attach phx-hook="PrelineInit" to any element rendering Preline UI markup, with a unique id (LiveView requires one on every hooked element). Add phx-update="ignore" alongside it for any plugin that holds runtime state, such as an open accordion panel, a selected Datepicker date, a dragged Range Slider handle, or Datatable's own JS-built pagination buttons. Without it, a LiveView patch to a completely unrelated part of the page can still reset that element back to its server-rendered markup, discarding state that only ever existed in the live DOM (an inline style Preline UI set, a class it toggled, markup its JS inserted). Re-running autoInit afterward will not repair that either, since it skips elements that already have a plugin instance.

    lib/my_app_web/components/actions_menu.html.heex
                              
                                <div id="actions-menu" phx-hook="PrelineInit" phx-update="ignore" class="hs-dropdown relative inline-flex">
                                  ...
                                </div>
                              
                            

    Datatable, Datepicker, Range Slider, and File Upload each depend on a third-party library, and each checks for its own dependency on window before it will initialize. See Optional dependencies in the practical guide before adding one of those plugins.

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.

CSS
                        
                          /* 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);
                        
                      

© 2026 Preline Labs.