AvenUI — 21 Phoenix LiveView Components
Even if AvenUI stopped being maintained tomorrow, your app keeps working — because mix aven_ui.add copies the code into your project. You own it. No runtime dependency. No paywalls. Ever.
Free alternative to Petal. MIT licensed. You own your components.
Why AvenUI?
- Zero JS by default — every component works with pure LiveView server rendering
- shadcn-style installer —
mix aven_ui.add button badgecopies code into your project so you own it - Full dark mode — CSS variable tokens, no Tailwind class flicker
- LiveView streams — Table and lists support
phx-update="stream"out of the box - Form-native — Input and Select integrate with
Phoenix.HTML.FormField+ Ecto changesets - Accessible — ARIA roles, keyboard navigation, focus management built in
- Tailwind v3 + v4 compatible
Accessibility
Every component includes:
roleandaria-*attributes (alert, modal, table, tabs, dropdown)- Keyboard navigation (Dropdown: Arrow keys, Escape, Tab)
- Focus management (Modal: focus trap on open, restore on close)
aria-expanded,aria-controls,aria-labelledbywhere relevant-
Screen reader announcements via
role="status"on Toast
Installation
1. Add dep
# mix.exs
def deps do
[
{:aven_ui, "~> 0.1", github: "khemmanat/aven_ui"}
]
end2. Install components
mix deps.get
# Add specific components
mix aven_ui.add button badge alert card input tabs modal
# Or add everything
mix aven_ui.add --all
# Dry run — see what would be copied
mix aven_ui.add --all --dry-run
Components are copied into lib/my_app_web/components/ui/ with the namespace
rewritten to MyAppWeb.UI.*. You own the code.
3. Import in web.ex
# lib/my_app_web.ex
defp html_helpers do
quote do
use AvenUI, :components
# or cherry-pick:
# import MyAppWeb.UI.Button
# import MyAppWeb.UI.Badge
end
end4. Add CSS
/* assets/css/app.css */
@import "./avenui.css";5. Add JS hooks
// assets/js/app.js
import { AvenUIHooks } from "./hooks/aven_ui";
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...AvenUIHooks, ...YourHooks },
});6. Add Tailwind preset
// assets/tailwind.config.js
const avenUIPreset = require("../../deps/aven_ui/assets/tailwind.config.js");
module.exports = {
presets: [avenUIPreset],
content: ["./lib/**/*.{ex,heex}", "./assets/js/**/*.js"],
};7. Add flash toasts to root layout
<%!-- lib/my_app_web/components/layouts/root.html.heex --%>
<body>
<%= @inner_content %>
<.flash_group flash={@flash} />
</body>Project structure
aven_ui/
├── mix.exs
├── lib/
│ ├── aven_ui.ex ← use AvenUI, :components macro
│ └── aven_ui/
│ ├── helpers.ex ← shared classes/1 utility
│ ├── components/
│ │ ├── accordion.ex
│ │ ├── alert.ex
│ │ ├── avatar.ex
│ │ ├── badge.ex
│ │ ├── button.ex
│ │ ├── card.ex
│ │ ├── code_block.ex
│ │ ├── dropdown.ex
│ │ ├── empty_state.ex
│ │ ├── input.ex
│ │ ├── kbd.ex
│ │ ├── modal.ex
│ │ ├── progress.ex
│ │ ├── separator.ex
│ │ ├── skeleton.ex
│ │ ├── spinner.ex
│ │ ├── stat.ex
│ │ ├── table.ex
│ │ ├── tabs.ex
│ │ ├── toast.ex
│ │ └── toggle.ex
│ └── mix/tasks/
│ └── add.ex ← mix aven_ui.add
├── assets/
│ ├── css/avenui.css ← --avn- CSS design tokens
│ ├── js/hooks/index.js ← AvenUIHooks
│ └── tailwind.config.js
└── storybook/ ← live component docs (Phoenix app)Components
Button
<.button>Deploy</.button>
<.button variant="secondary" size="sm">Cancel</.button>
<.button variant="danger" phx-click="delete" phx-value-id={@id}>Delete</.button>
<.button loading={@saving}>Saving…</.button>Variants:primarysecondaryghostdangeroutlinelink
Sizes:xssmmdlgxl
Badge
<.badge>Default</.badge>
<.badge variant="success"><.badge_dot /> Online</.badge>
<.badge variant="danger">Expired</.badge>Variants:defaultprimarysuccesswarningdangerinfooutline
Alert
<.alert variant="success" title="Deployed!">Version 2.4.1 is live.</.alert>
<.alert variant="warning" title="High memory" dismissible phx-click="dismiss">
Node #3 is at 87%.
</.alert>Input + Select
<.input field={@form[:email]} type="email" label="Email" hint="We'll never share this." />
<.input field={@form[:amount]} label="Amount">
<:prefix>฿</:prefix>
</.input>
<.select field={@form[:region]} label="Region"
options={["Bangkok", "Singapore", "Tokyo"]} prompt="Choose…" />Card
<.card>
<:header>
<.card_title>Server #3</.card_title>
<.card_description>Last ping 2s ago</.card_description>
</:header>
<:body>
<.progress value={72} label="CPU" show_value />
</:body>
<:footer>
<.button size="sm">Restart</.button>
</:footer>
</.card>Modal
<.button phx-click="open_modal">Open</.button>
<.modal :if={@show_modal} id="confirm-modal" on_close="close_modal">
<:title>Delete project?</:title>
<:description>This cannot be undone.</:description>
<p>All data will be permanently deleted.</p>
<:footer>
<.button variant="ghost" phx-click="close_modal">Cancel</.button>
<.button variant="danger" phx-click="confirm_delete">Delete</.button>
</:footer>
</.modal>In LiveView:
def handle_event("open_modal", _, socket), do: {:noreply, assign(socket, show_modal: true)}
def handle_event("close_modal", _, socket), do: {:noreply, assign(socket, show_modal: false)}Dropdown
<.dropdown id="actions-menu">
<:trigger>
<.button variant="secondary" size="sm">Options <.dropdown_chevron /></.button>
</:trigger>
<.dropdown_label>Actions</.dropdown_label>
<.dropdown_item phx-click="edit">Edit</.dropdown_item>
<.dropdown_separator />
<.dropdown_item variant="danger" phx-click="delete">Delete</.dropdown_item>
</.dropdown>Tabs
<.tabs active={@tab} patch="/dashboard" param="tab">
<:tab id="overview">Overview</:tab>
<:tab id="settings">Settings</:tab>
<:panel id="overview"><.overview /></:panel>
<:panel id="settings"><.settings_form /></:panel>
</.tabs>Variants:underlinepillsboxed
Table
<.table rows={@deployments} sort_field={@sort_by} sort_dir={@sort_dir}>
<:col :let={row} label="Commit" field="sha" sortable>
<code class="font-mono text-xs"><%= row.sha %></code>
</:col>
<:col :let={row} label="Status">
<.badge variant={status_color(row.status)}><%= row.status %></.badge>
</:col>
<:action :let={row}>
<.button size="xs" variant="ghost" phx-click="restart" phx-value-id={row.id}>
Restart
</.button>
</:action>
</.table>
<.pagination page={@page} total_pages={@total_pages} phx-click="paginate" />Toast / Flash
<%!-- In root.html.heex — renders all @flash messages as toasts --%>
<.flash_group flash={@flash} />From LiveView:
socket |> put_flash(:success, "Deployment complete!")
socket |> put_flash(:error, "Connection failed.")Accordion
<.accordion id="faq">
<:item title="Is AvenUI free?">Yes. MIT licensed.</:item>
<:item title="Does it support dark mode?" open>
Yes — via CSS variables, no Tailwind class flicker.
</:item>
</.accordion>Avatar
<.avatar initials="KN" />
<.avatar initials="KN" size="lg" color="green" />
<.avatar src="https://..." alt="Khemmanat" />
<.avatar_group>
<.avatar initials="KN" />
<.avatar initials="AB" color="amber" />
<.avatar initials="+3" color="gray" />
</.avatar_group>Stat
<div class="grid grid-cols-3 gap-4">
<.stat label="Deploys today" value="24" change="+8" trend="up" />
<.stat label="Avg response" value="142" suffix="ms" change="+12ms" trend="down" />
<.stat label="Uptime (30d)" value="99.97" suffix="%" />
</div>Utility components
<.progress value={72} label="Storage" show_value color="blue" />
<.skeleton class="h-4 w-48" />
<.spinner size="lg" class="text-avn-purple" />
<.separator label="or continue with" />
<.kbd>⌘</.kbd><.kbd>K</.kbd>
<.empty_state title="No results" description="Try a different search." />
<.code_block lang="elixir" copyable>def hello, do: "world"</.code_block>JS Hooks
| Hook | Purpose |
|---|---|
Dropdown | Keyboard nav, outside-click close |
Modal | Focus trap, scroll lock, Escape |
Tooltip | Position-aware tooltip |
Flash | Auto-dismiss with pause-on-hover |
AutoResize | Growing textarea |
CopyToClipboard | Clipboard API with feedback |
InfiniteScroll | Load-more on sentinel visible |
ScrollTop | Smooth scroll on LiveView patch |
<%!-- Tooltip usage --%>
<button phx-hook="Tooltip" id="info-btn" data-tooltip="More info">?</button>
<%!-- Dropdown usage --%>
<div phx-hook="Dropdown" id="my-menu">
<button data-avn-dropdown-trigger>Open</button>
<div data-avn-dropdown-menu hidden>
<button data-avn-dropdown-item>Edit</button>
</div>
</div>Storybook
Run the interactive component doc site locally:
cd storybook
mix deps.get
mix phx.server
# Open http://localhost:4000Roadmap
- Command Palette / Combobox
- Drawer / Slideout panel
- Date Picker
- Chart hooks (VegaLite + Chart.js)
- Multi-select
- File upload
- Publish to Hex.pm
License
MIT — free to use, modify, and distribute.