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?

Accessibility

Every component includes:


Installation

1. Add dep

# mix.exs
def deps do
  [
    {:aven_ui, "~> 0.1", github: "khemmanat/aven_ui"}
  ]
end

2. 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
end

4. 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:4000

Roadmap


License

MIT — free to use, modify, and distribute.