Even if AvenUI stopped being maintained tomorrow, your app keeps working — because
mix aven_ui.addcopies the code into your project. You own it. No paywalls. Ever.
Free alternative to Petal. MIT licensed. Tailwind v4 native.
- 24 accessible Phoenix LiveView components
- shadcn-style installer —
mix aven_ui.add button badgecopies source into your app mix aven_ui.newscaffolds a fresh Phoenix 1.8 app with AvenUI pre-wired- Tailwind v4 / Phoenix 1.8 native — CSS-first setup, no
tailwind.config.jsrequired - Full dark mode via design tokens, without class flicker
- Form-native inputs for
Phoenix.HTML.FormFieldand Ecto changesets - Rich data UI:
combobox,date_picker, anddata_table - Minimal JS hooks only where interaction is actually needed
combobox— searchable select with keyboard navigationdate_picker— single-date and range picker with server-rendered calendardata_table— filter bar, server-side sort, pagination, and active filter chipsmix aven_ui.new— Phoenix 1.8 project generator with AvenUI pre-installed- Tailwind v4 migration — CSS-first setup with native
@themetokens
Every component ships with accessibility baked in:
roleandaria-*attributes where relevant- Keyboard navigation for dropdowns, comboboxes, tabs, and date pickers
- Focus management for modal dialogs
- Screen-reader friendly flash toasts via
role="status" - Semantic table, form, and navigation markup
mix aven_ui.new my_app
cd my_app
mix phx.serverThis generates a Phoenix 1.8 app with:
- AvenUI installed
- all 24 components added
- Tailwind v4 CSS wired
AvenUIHooksconnected inapp.js- starter dashboard layout with dark mode toggle
# mix.exs
def deps do
[
{:aven_ui, "~> 0.2.3", hex: :aven_ui}
]
endIf you prefer GitHub:
{:aven_ui, "~> 0.2.3", github: "khemmanat/aven_ui"}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-runThis copies component modules into lib/my_app_web/components/ui/, plus:
assets/css/avenui.cssassets/js/hooks/aven_ui.js
# lib/my_app_web.ex
defp html_helpers do
quote do
import MyAppWeb.UI.Accordion
import MyAppWeb.UI.Alert
import MyAppWeb.UI.Avatar
import MyAppWeb.UI.Badge
import MyAppWeb.UI.Button
import MyAppWeb.UI.Card
import MyAppWeb.UI.CodeBlock
import MyAppWeb.UI.Combobox
import MyAppWeb.UI.DataTable
import MyAppWeb.UI.DatePicker
import MyAppWeb.UI.Dropdown
import MyAppWeb.UI.EmptyState
import MyAppWeb.UI.Input
import MyAppWeb.UI.Kbd
import MyAppWeb.UI.Modal
import MyAppWeb.UI.Progress
import MyAppWeb.UI.Separator
import MyAppWeb.UI.Skeleton
import MyAppWeb.UI.Spinner
import MyAppWeb.UI.Stat
import MyAppWeb.UI.Table
import MyAppWeb.UI.Tabs
import MyAppWeb.UI.Toast
import MyAppWeb.UI.Toggle
end
endIf you want to use AvenUI directly from the dependency instead of your copied modules, use AvenUI, :components is also available.
/* assets/css/app.css */
@import "tailwindcss";
@import "./avenui.css";No tailwind.config.js is needed with Tailwind v4.
// assets/js/app.js
import { AvenUIHooks } from "./hooks/aven_ui";
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...AvenUIHooks, ...Hooks },
});<%!-- lib/my_app_web/components/layouts/root.html.heex --%>
<body>
<%= @inner_content %>
<.flash_group flash={@flash} />
</body>aven_ui/
├── mix.exs
├── lib/
│ ├── aven_ui.ex
│ └── aven_ui/
│ ├── data_table.ex
│ ├── helpers.ex
│ ├── components/
│ │ ├── accordion.ex
│ │ ├── alert.ex
│ │ ├── avatar.ex
│ │ ├── badge.ex
│ │ ├── button.ex
│ │ ├── card.ex
│ │ ├── code_block.ex
│ │ ├── combobox.ex
│ │ ├── data_table.ex
│ │ ├── date_picker.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
│ ├── list.ex
│ └── new.ex
├── assets/
│ ├── css/avenui.css
│ └── js/hooks/index.js
└── storybook/
- Input / Select
- Combobox
- Date Picker
- Toggle
- Button
- Dropdown
- Tabs
- Accordion
- Table
- Data Table
- Card
- Stat
- Alert
- Toast / Flash
- Modal
- Avatar
- Badge
- Code Block
- Empty State
- Kbd
- Progress
- Separator
- Skeleton
- Spinner
<.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: primary secondary ghost danger outline link
Sizes: xs sm md lg xl
<.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", "bangkok"}, {"Singapore", "singapore"}, {"Tokyo", "tokyo"}]}
prompt="Choose..."
/><.combobox
id="country"
field={@form[:country]}
label="Country"
placeholder="Search countries..."
options={[
%{value: "th", label: "Thailand"},
%{value: "sg", label: "Singapore"},
%{value: "jp", label: "Japan"}
]}
/>
<.combobox
id="user"
name="user_id"
label="User"
placeholder="Search users..."
options={@users}
selected={@user_id}
on_search="search_users"
/><.date_picker
id="birthday"
name="birthday"
label="Birthday"
selected={@birthday}
view_month={@dp_month}
on_change="date_selected"
/>
<.date_picker
id="booking"
name="booking"
label="Stay dates"
mode="range"
range_start={@check_in}
range_end={@check_out}
view_month={@dp_month}
on_change="dates_selected"
/>def handle_event("date_selected", %{"date" => date}, socket) do
{:noreply, assign(socket, :birthday, Date.from_iso8601!(date))}
end
def handle_event("dp_prev_month_birthday", _, socket) do
{:noreply, update(socket, :dp_month, &Date.beginning_of_month(Date.add(&1, -1)))}
end
def handle_event("dp_next_month_birthday", _, socket) do
{:noreply, update(socket, :dp_month, &Date.beginning_of_month(Date.add(&1, 32)))}
end<.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><.button phx-click="open_modal">Open</.button>
<.modal :if={@show_modal} id="confirm-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>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 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 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: underline pills boxed
<.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" /><.data_table
id="users-table"
rows={@rows}
table={@table}
title="Users"
on_filter="dt_filter"
on_sort="dt_sort"
on_paginate="dt_paginate"
>
<:filter>
<.dt_search name="search" placeholder="Search users..." />
<.dt_select
name="status"
prompt="All statuses"
options={[{"Active", "active"}, {"Inactive", "inactive"}]}
/>
</:filter>
<:toolbar>
<.button size="sm" phx-click="new_user">New user</.button>
</:toolbar>
<:col :let={u} label="Name" field="name" sortable><%= u.name %></:col>
<:col :let={u} label="Email" field="email" sortable><%= u.email %></:col>
<:action :let={u}>
<.button size="xs" variant="ghost" phx-click="edit" phx-value-id={u.id}>Edit</.button>
</:action>
</.data_table>alias AvenUI.DataTable
def mount(_, _, socket) do
{:ok, socket |> assign(:table, DataTable.new(per_page: 20)) |> load_rows()}
end
def handle_event("dt_filter", params, socket) do
table =
socket.assigns.table
|> DataTable.set_filter(params)
|> DataTable.reset_page()
{:noreply, socket |> assign(:table, table) |> load_rows()}
end
def handle_event("dt_sort", %{"field" => field}, socket) do
table = DataTable.toggle_sort(socket.assigns.table, field)
{:noreply, socket |> assign(:table, table) |> load_rows()}
end
def handle_event("dt_paginate", %{"page" => page}, socket) do
table = DataTable.set_page(socket.assigns.table, String.to_integer(page))
{:noreply, socket |> assign(:table, table) |> load_rows()}
end<.flash_group flash={@flash} />socket |> put_flash(:info, "Deployment complete!")
socket |> put_flash(:error, "Connection failed.")<.accordion id="faq">
<:item title="Is AvenUI free?">Yes. MIT licensed.</:item>
<:item title="Does it support dark mode?" open>
Yes — via CSS variables and Tailwind v4 theme tokens.
</:item>
</.accordion><.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><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><.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>AvenUI exports 10 lightweight hooks:
| Hook | Purpose |
|---|---|
Flash |
Auto-dismiss with pause on hover |
Dropdown |
Keyboard nav, outside click close |
Modal |
Focus trap, Escape close, scroll lock |
Tooltip |
Position-aware tooltip |
AutoResize |
Growing textarea |
CopyToClipboard |
Clipboard API with feedback |
InfiniteScroll |
Load-more on sentinel visible |
ScrollTop |
Smooth scroll on LiveView patch |
AvenUICombobox |
Open, search, select, keyboard nav |
AvenUIDatePicker |
Date-picker open/close and clear wiring |
<button phx-hook="Tooltip" id="info-btn" data-tooltip="More info">?</button>
<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>Run the interactive component docs locally:
cd storybook
mix deps.get
mix phx.serverThen open http://localhost:4000.
- Command palette
- Drawer / slideout panel
- Chart hooks
- Multi-select
- File upload
MIT — free to use, modify, and distribute.