Pine UI 🌲
A Tailwind CSS + Alpine.js component library for Phoenix LiveView. All 42 Pines UI elements as 49 Phoenix function components — modal, slide-over, dropdown menu, command palette, toast, table, date picker, tabs, accordion, select, file upload and more. Accessible, dark-mode ready, and usable by AI coding agents out of the box.
Pines is DevDojo's Alpine.js + Tailwind CSS component library, distributed as HTML you copy
and paste. This package turns it into real Phoenix components with declarative attr/slot
APIs, compile-time validation, dark mode, and the LiveView plumbing Alpine needs to survive
DOM patching.
<.card title="Getting started">
<p>Every Pines element, as a Phoenix component.</p>
<:footer>
<.button phx-click={JS.dispatch("pine:open", to: "#docs")}>Read the docs</.button>
</:footer>
</.card>
<.slide_over id="docs" title="Documentation">…</.slide_over>
Installation
def deps do
[{:pine_ui_phoenix, "~> 0.2.0"}]
end
Requires Elixir 1.15+ and phoenix_live_view >= 0.20.4 — which covers 0.20.x, 1.0, 1.1
and 1.2.
Three further steps are required. Skipping them is the difference between working components and components that render unstyled or flash their contents on load:
Point Tailwind at the package, or every class it uses is purged from your production build.
// Tailwind v3 — tailwind.config.jscontent: [..., "../deps/pine_ui_phoenix/lib/**/*.ex"]/* Tailwind v4 — app.css */@source "../../deps/pine_ui_phoenix/lib";Import the stylesheet, which carries the
[x-cloak]rule and the marquee keyframes.@import "../../deps/pine_ui_phoenix/priv/static/pine_ui.css";Install Alpine and wire up
pineDom(), so Alpine state survives LiveView patches.import { pineDom, PineHooks } from "../../deps/pine_ui_phoenix/priv/static/pine_ui.js"const liveSocket = new LiveSocket("/live", Socket, {params: { _csrf_token: csrfToken },hooks: { ...PineHooks },dom: pineDom()})
Full details, including which Alpine plugins each component needs, are in the installation guide.
Usage
defp html_helpers do
quote do
use Phoenix.Component
use PineUiPhoenix
# ...
end
end
A stock Phoenix app already generates its own <.button>, <.table> and <.modal> in
CoreComponents, so resolve the clash however suits you:
use PineUiPhoenix, except: [:button, :table, :modal]
use PineUiPhoenix, only: [:command, :marquee, :rating]
use PineUiPhoenix, prefix: :pine # <.pine_button>
Components
All 42 Pines elements, across both the "Tailwind and Alpine" and "Tailwind Only" groups.
| Layout | card · table · blockquote |
| Forms | button · text_input · textarea · checkbox · radio_group · select · select_menu · combobox · tags_input · switch · range_slider · rating · date_picker · copy_to_clipboard |
| Feedback | alert · badge · banner · progress · progress_circle · progress_steps · toast_group · toast_trigger |
| Navigation | accordion · tabs · breadcrumbs · pagination · navigation_menu · menubar |
| Overlays | modal · full_screen_modal · slide_over · dropdown_menu · context_menu · popover · hover_card · tooltip · command |
| Media | image_gallery · carousel · video · monaco_editor |
| Animation | marquee · retro_grid · typing_effect · text_animation |
Fourteen of them need no JavaScript at all.
What this port changes
It is a port, not a transcription. Where the copy-paste original made sense as HTML but not as a component, it was rebuilt:
- The calendar grid is computed in Elixir. Pines builds it in Alpine; here
Datehandles leap years and month lengths, and the days are real<button>s in the DOM. text_animationneeds no GSAP. The original loads GSAP from a CDN; this splits the text server-side and animates with CSS transitions.- Alpine state is JSON-encoded, never interpolated. Building
x-databy string interpolation breaks on apostrophes and allows expression injection. - Classes merge rather than concatenate, so
<.button class="bg-red-500">actually overrides the default background. See theming. - Accessibility is wired properly — real ARIA bindings, unique ids, keyboard navigation,
and
prefers-reduced-motion.
Using it with AI coding agents
The package ships usage-rules.md
and a Claude Code skill, so agents get the component API and the install gotchas without
guessing. If you use usage_rules:
{:usage_rules, "~> 1.0", only: [:dev]}
mix usage_rules.sync AGENTS.md pine_ui_phoenix --link-to-folder deps
That pulls the rules into your AGENTS.md/CLAUDE.md. Without it, agents still read
deps/pine_ui_phoenix/usage-rules.md directly, and hexdocs publishes
llms.txt for the same purpose.
The component table in those rules is generated from the live attr/slot metadata and
verified by a test, so it cannot drift from the real API.
Documentation
- Installation — Tailwind, Alpine, plugins, hooks
- LiveView integration — DOM patching, server-driven state, forms, streams
- Theming — class merging, dark mode, accent colours
- Upgrading to v0.2 — from v0.1.x
Component reference: hexdocs.pm/pine_ui_phoenix.
Upgrading from v0.1.x
Every v0.1.x function name still works and is re-exported as a deprecated delegate, so your
templates keep rendering. Three things do change — the LiveView floor, the palette, and the
removal of :poison. See the upgrade guide.
Development
mix deps.get
mix test # component + cross-component contract tests
mix lint # format check, warnings-as-errors, credo
mix dev # demo app at http://localhost:4444
mix assets.watch # rebuild the demo's Tailwind CSS
mix dev runs a demo of every component with its source shown alongside each example. It
serves the shippedpriv/static assets, so it exercises exactly what consumers get.
Credits
Design and behaviour come from Pines by DevDojo. This is an independent Phoenix port.
License
MIT — see LICENSE.