Phoenix LiveView components with the LineOne design (Tailwind CSS v4), used the way Fluxon is
used: use LineaUI, one function per component, class that overrides the defaults, overlays
opened by id, and hooks that survive LiveView patches. No JavaScript dependencies.
Credits. The code of this package is DevAction's. The visual design it reproduces derives from LineOne by PiniaStudio (ThemeForest) and is credited to its author. See LICENSE.
Installation
# mix.exs
{:linea_ui, "~> 0.1"}/* assets/css/app.css — remove daisyUI and its theme plugins */
@import "tailwindcss";
@import "../../deps/linea_ui/assets/css/fonts.css";
@import "../../deps/linea_ui/assets/css/linea_ui.css";// assets/js/app.js
import {Hooks as LineaHooks} from "linea_ui"
const liveSocket = new LiveSocket("/live", Socket, {
hooks: {...colocatedHooks, ...LineaHooks},
params: {_csrf_token: csrfToken},
})# lib/my_app_web/endpoint.ex — serves the Poppins and Inter fonts
plug Plug.Static, at: "/linea_ui", from: {:linea_ui, "priv/static"}
# lib/my_app_web.ex, inside html_helpers/0
use LineaUI, avoid_conflicts: true # keeps CoreComponents' button/input/table/label/error/headerDark mode is LineOne's: the dark class on <html>, remembered in localStorage["dark-mode"].
To avoid a flash, put this in the root layout's <head>:
<script>localStorage.getItem("dark-mode") === "dark" && document.documentElement.classList.add("dark")</script>Errors are translated with the app's own function:
config :linea_ui, :translate_error, &MyAppWeb.CoreComponents.translate_error/1Icons
The small glyphs the components draw themselves (the chevron of a select, the cross of a toast, the check of a checkbox, the arrows of a pager, the tools of the text editor...) are LineOne's own SVGs by default. An app can have them drawn with Hugeicons (the free Stroke Rounded set) instead:
# mix.exs: the SVGs are vendored, not compiled
{:hugeicons, github: "hugeicons/hugeicons", ref: "3365154e0ae2461fbfb6249b89649127207a4f9e",
sparse: "icons", app: false, compile: false, depth: 1}
# config/config.exs
config :linea_ui, :icons, :hugeicons # :lineone is the default/* assets/css/app.css, after the linea_ui.css import: turns every huge-* class into a CSS mask */
@plugin "../../deps/linea_ui/assets/css/hugeicons.cjs" {
dir: "deps/hugeicons/icons";
}dir is the folder of the SVGs, relative to where Tailwind runs (the app's root). The setting is read when
the page renders; the components whose hooks draw glyphs in the browser (toast, date picker) get it from a
data-icons attribute. Restart the app after changing it. Glyphs that are artwork (the two-tone icons of the
sidebar, the arrows of tooltips and popovers, the spinner) and the carets and check marks LineOne bakes into
its CSS (native select, tom-select, checkbox, radio, switch, table sorting) stay LineOne's in both sets.
For your own icons use <.icon name="huge-search-01" class="size-5" /> (any other class name, such as
fa-solid fa-house, passes through). Write the name in full in the source, never interpolated: Tailwind
only sees what is written, and a name that is not in the set renders nothing.
Claude Code skill
mix linea_ui.install # after adding the dependency, and after every upgrade
writes .claude/skills/linea-ui/ — a short SKILL.md index (the rules every component shares
and one line per component) and references/<component>.md with the attrs, allowed values,
slots and examples of each, generated from the installed components themselves, so it never
drifts from the code. Only the index is loaded up front; a reference is read when its component
is used. The task also points AGENTS.md/CLAUDE.md at the skill (once) and prints the
remaining setup lines. mix linea_ui.gen.skill --path <dir> writes only the skill.
Usage
<.button color="primary" phx-click={LineaUI.open("new-order")}>New order</.button>
<.modal id="new-order">
<.form for={@form} phx-submit="save">
<.input field={@form[:customer]} label="Customer" />
<.button type="submit" color="primary">Save</.button>
</.form>
</.modal>{:noreply, LineaUI.close(socket, "new-order")}Development
mix test && mix js.test # package
cd preview && LINEA_PREVIEW_PORT=4200 mix phx.server
preview/ is a Phoenix app with one page per LineOne demo page; the original LineOne page is
served next to it at /lineone/<page>.html. node scripts/shot.mjs takes the screenshots used to
compare them. CONVENTIONS.md is the guide for writing components.