PhoenixFilament lets you go from an Ecto schema to a fully-functional admin interface in minutes. This guide walks you through installation, your first resource, and common customizations.
Prerequisites
Before starting, you need:
- A Phoenix 1.7+ application with at least one Ecto schema
- Phoenix LiveView installed and configured
- Tailwind CSS configured (Phoenix 1.7+ includes it by default)
- daisyUI 5 (optional but recommended — Phoenix 1.8 includes it by default)
If you are on Phoenix 1.8, all of the above are included out of the box.
Installation
1. Add dependencies
Add PhoenixFilament and Igniter to your mix.exs:
def deps do
[
{:phoenix_filament, "~> 0.1"},
{:igniter, "~> 0.7"} # required for the installer
]
endFetch your dependencies:
mix deps.get
2. Run the installer
mix phx_filament.install
The installer is idempotent — safe to run multiple times.
What the Installer Creates
Running mix phx_filament.install creates three files:
lib/my_app_web/admin.ex — your Panel module:
defmodule MyAppWeb.Admin do
use PhoenixFilament.Panel,
path: "/admin",
brand_name: "MyApp Admin"
# Add resources with: mix phx_filament.gen.resource MyApp.Schema
endassets/vendor/chart.min.js — the Chart.js library for chart widgets.
assets/js/phx_filament_hooks.js — the LiveView hook that powers Chart widgets.
Router Setup
Add two lines to your router.ex:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# 1. Import the panel router macro
import PhoenixFilament.Panel.Router
pipeline :browser do
# ... your existing pipeline
end
scope "/" do
pipe_through :browser
# 2. Mount the panel
phoenix_filament_panel "/admin", MyAppWeb.Admin
end
endThe phoenix_filament_panel/2 macro registers all CRUD routes, the dashboard, and any
plugin routes automatically.
Icons
PhoenixFilament uses Heroicons for sidebar and widget icons.
Icons are referenced by their CSS class name (e.g., hero-document-text).
Phoenix 1.8 apps include Heroicons by default through the assets/vendor/heroicons/
directory. If your app was generated with Phoenix 1.8, icons work out of the box.
For older Phoenix apps or custom setups, ensure Heroicons are available:
- The
heroiconspackage must be in your assets - CSS classes like
hero-document-textmust resolve to SVG icons - See the Phoenix Components documentation for setup details
When registering resources, specify icons with the hero- prefix:
resources do
resource MyAppWeb.Admin.PostResource,
icon: "hero-document-text"
resource MyAppWeb.Admin.UserResource,
icon: "hero-users"
endResources without an icon: option display the first letter of their label as a fallback.
Hook Setup
Open assets/js/app.js and import the PhoenixFilament hooks:
import PhxFilamentHooks from "./phx_filament_hooks"
// Find your LiveSocket initialization and merge the hooks:
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
// Merge PhoenixFilament hooks with any existing hooks:
hooks: {...Hooks, ...PhxFilamentHooks}
})If you don't have any custom hooks yet, the hooks object will simply be PhxFilamentHooks.
Your First Resource
Generate a resource for an existing Ecto schema:
mix phx_filament.gen.resource MyApp.Blog.Post
This creates lib/my_app_web/admin/post_resource.ex:
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo
endResource registration in your Panel
mix phx_filament.gen.resource automatically registers the resource in your Panel module. You should see a resources do...end block added to lib/my_app_web/admin.ex:
defmodule MyAppWeb.Admin do
use PhoenixFilament.Panel,
path: "/admin",
brand_name: "MyApp Admin"
resources do
resource MyAppWeb.Admin.PostResource,
icon: "hero-document-text",
nav_group: "Blog"
end
endIf auto-registration fails (e.g. the Panel module cannot be found), the task prints instructions for manual registration — simply add the resources do...end block above yourself.
Visit /admin
Start your Phoenix server:
mix phx.server
Navigate to http://localhost:4000/admin. You will see:
- A sidebar with "Posts" listed under "Blog"
- An index page with a table of all posts, with search, sort, and pagination
- Create / Edit / View / Delete actions out of the box
PhoenixFilament auto-derives columns and form fields from your Ecto schema — no additional configuration required to get a working interface.
Customizing Forms
By default, PhoenixFilament infers form fields from your schema's Ecto field types. To
customize, add a form do...end block to your resource:
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo,
label: "Post",
plural_label: "Posts"
form do
text_input :title, label: "Title", placeholder: "Enter post title"
textarea :body, label: "Body"
toggle :published, label: "Published"
date :published_at, label: "Publish Date"
end
endAvailable field types
| Macro | Description |
|---|---|
text_input :name | Single-line text input |
textarea :name | Multi-line text area |
number_input :name | Numeric input |
select :name, options: [...] | Drop-down select |
checkbox :name | Checkbox (boolean) |
toggle :name | Toggle switch (boolean) |
date :name | Date picker |
datetime :name | Date + time picker |
hidden :name | Hidden field |
All field macros accept these common options:
label:— override the auto-derived labelplaceholder:— input placeholder text
Form sections
Group related fields with section:
form do
section "Basic Info" do
text_input :title
textarea :body
end
section "Publishing" do
toggle :published
date :published_at
end
endMulti-column layout
Use columns to render fields side by side:
form do
columns 2 do
text_input :first_name
text_input :last_name
end
textarea :bio
endConditional field visibility
Show or hide a field based on another field's value using visible_when:
form do
toggle :published
date :published_at, visible_when: [field: :published, eq: true]
endvisible_when accepts field: (the field to watch) and eq: (the value that makes this
field visible). The check happens in real-time as the user fills in the form.
You can also apply visible_when to an entire section:
form do
toggle :published
section "Schedule", visible_when: [field: :published, eq: true] do
date :published_at
select :timezone, options: ["UTC", "America/New_York", "Europe/Berlin"]
end
endCustomizing Tables
Add a table do...end block to your resource to customize the index listing:
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo
table do
column :title, label: "Title"
column :published, label: "Published"
column :inserted_at, label: "Created"
actions do
action :view, label: "View", icon: "hero-eye"
action :edit, label: "Edit", icon: "hero-pencil"
action :delete, label: "Delete", icon: "hero-trash", confirm: "Are you sure?"
end
filters do
boolean_filter :published, label: "Published"
select_filter :author_id, label: "Author", options: [{"Alice", 1}, {"Bob", 2}]
end
end
endColumns
Each column declaration renders a column in the index table.
column :field_name # auto-derives label
column :field_name, label: "Custom Label" # explicit labelActions
The actions do...end block defines the per-row action buttons.
actions do
action :view # View action (shows the record)
action :edit # Edit action (opens edit form)
action :delete, confirm: "Delete this post?" # Delete with confirmation dialog
action :archive, label: "Archive", icon: "hero-archive-box" # Custom action
endBuilt-in action types (:view, :edit, :delete) are handled automatically.
Custom action types dispatch {:table_action, action, id} to your resource's handle_info/2,
which you can override:
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo
@impl true
def handle_info({:table_action, :archive, id}, socket) do
MyApp.Blog.archive_post(id)
{:noreply, Phoenix.LiveView.put_flash(socket, :info, "Post archived")}
end
def handle_info(msg, socket), do: super(msg, socket)
endFilters
The filters do...end block renders a filter toolbar above the table.
filters do
boolean_filter :published # true / false toggle
select_filter :status, options: [{"Draft", "draft"}, {"Published", "published"}]
date_filter :inserted_at, label: "Created After" # date range picker
endSearch
Full-text search across string columns is enabled by default. The search box appears in the
table header and searches across all :string fields in your schema.
Pagination and Sorting
Pagination and column sorting are enabled automatically. Clicking any column header toggles ascending/descending sort. Pagination controls appear below the table.
Dashboard Widgets
The dashboard (the page you land on at /admin) supports four widget types:
StatsOverview, Chart, Table, and Custom.
StatsOverview widget
Shows stat cards with optional icons, colors, and sparklines.
defmodule MyAppWeb.Admin.OverviewStats do
use PhoenixFilament.Widget.StatsOverview
@impl true
def stats(_assigns) do
[
stat("Total Posts", MyApp.Repo.aggregate(MyApp.Blog.Post, :count),
icon: "hero-document-text",
color: :success,
description: "#{new_today()} new today"),
stat("Published", published_count(),
icon: "hero-check-circle",
color: :info),
stat("Draft", draft_count(),
icon: "hero-pencil-square",
color: :warning)
]
end
defp new_today, do: 0 # implement with real query
defp published_count, do: 0 # implement with real query
defp draft_count, do: 0 # implement with real query
endstat/2 and stat/3 build stat card data. Options for stat/3:
| Option | Values | Description |
|---|---|---|
icon: | Heroicon name | Icon displayed in the stat card |
color: | :success, :error, :warning, :info | Value text color |
description: | String | Subtitle text below the value |
chart: | List of numbers | Renders an inline sparkline |
Chart widget
Renders a Chart.js chart. Requires the hook setup described above.
defmodule MyAppWeb.Admin.PostsChart do
use PhoenixFilament.Widget.Chart
@impl true
def chart_type, do: :bar
@impl true
def chart_data(_assigns) do
%{
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
datasets: [
%{
label: "Posts Published",
data: [12, 19, 8, 25, 14, 30],
backgroundColor: "rgba(99, 102, 241, 0.5)"
}
]
}
end
# Optional: override default Chart.js options
def chart_options do
%{responsive: true, plugins: %{legend: %{position: "top"}}}
end
endSupported chart_type values: :line, :bar, :pie, :doughnut
Table widget
Renders a read-only table on the dashboard.
defmodule MyAppWeb.Admin.RecentPosts do
use PhoenixFilament.Widget.Table
@impl true
def heading, do: "Recent Posts"
@impl true
def columns do
[
PhoenixFilament.Column.column(:title, label: "Title"),
PhoenixFilament.Column.column(:inserted_at, label: "Date")
]
end
@impl true
def update(assigns, socket) do
{:ok, socket} = super(assigns, socket)
rows = MyApp.Repo.all(
from p in MyApp.Blog.Post,
order_by: [desc: p.inserted_at],
limit: 10
)
{:ok, Phoenix.Component.assign(socket, :rows, rows)}
end
endCustom widget
For completely custom dashboard content, use PhoenixFilament.Widget.Custom:
defmodule MyAppWeb.Admin.WelcomeWidget do
use PhoenixFilament.Widget.Custom
@impl true
def render(assigns) do
~H"""
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title">Welcome to the Admin Panel</h2>
<p>You are logged in as {@current_user.email}.</p>
</div>
</div>
"""
end
endRegistering widgets in your Panel
defmodule MyAppWeb.Admin do
use PhoenixFilament.Panel,
path: "/admin",
brand_name: "MyApp Admin"
resources do
resource MyAppWeb.Admin.PostResource, icon: "hero-document-text"
end
widgets do
widget MyAppWeb.Admin.OverviewStats, sort: 1, column_span: :full
widget MyAppWeb.Admin.PostsChart, sort: 2, column_span: 6
widget MyAppWeb.Admin.RecentPosts, sort: 3, column_span: 6
end
endWidget options:
| Option | Default | Description |
|---|---|---|
sort: | 0 | Rendering order (ascending) |
column_span: | 12 (full width) | Grid column span: 1–12 or :full |
Theming
PhoenixFilament uses daisyUI's theme system. Set a theme per panel:
use PhoenixFilament.Panel,
path: "/admin",
brand_name: "MyApp Admin",
theme: "corporate"Popular daisyUI themes: light, dark, corporate, retro, cyberpunk, cupcake,
bumblebee, emerald, synthwave, dracula, night, dim, nord, sunset.
Dark mode toggle
Enable a built-in light/dark toggle button in the panel header:
use PhoenixFilament.Panel,
path: "/admin",
theme: "corporate",
theme_switcher: trueWhen theme_switcher: true, a sun/moon icon button appears in the top navigation bar.
It uses daisyUI's theme-controller — clicking it toggles between the configured theme
and dark.
Authentication
PhoenixFilament does not bundle an auth solution — it integrates with whatever your app already uses.
LiveView on_mount hook
The recommended approach is to use a LiveView on_mount hook. If you use phx.gen.auth,
the generated UserAuth module includes an on_mount/4 callback:
defmodule MyAppWeb.Admin do
use PhoenixFilament.Panel,
path: "/admin",
on_mount: {MyAppWeb.UserAuth, :require_authenticated_user},
brand_name: "MyApp Admin"
resources do
resource MyAppWeb.Admin.PostResource, icon: "hero-document-text"
end
endThe on_mount: option must be a {Module, :function} tuple. The function must match the
Phoenix LiveView on_mount callback signature:
def on_mount(:require_authenticated_user, _params, session, socket) do
# validate session, assign current_user, or redirect
{:cont, assign(socket, :current_user, user)}
# or {:halt, redirect(socket, to: "/login")}
endHTTP-level authentication
For HTTP-level protection (guards against non-LiveView requests), use pipe_through:
scope "/admin" do
pipe_through [:browser, :require_authenticated_user]
phoenix_filament_panel "/", MyAppWeb.Admin
endSession revocation
To disconnect all live sessions for a user (e.g. after password change, logout-everywhere):
# In your UserAuth or session management code:
PhoenixFilament.Panel.revoke_sessions(MyApp.PubSub, current_user.id)This requires the pubsub: option on your panel:
use PhoenixFilament.Panel,
path: "/admin",
pubsub: MyApp.PubSub,
on_mount: {MyAppWeb.UserAuth, :require_authenticated_user}Plugins
Plugins let you add custom navigation, routes, and widgets to a panel without modifying the panel module directly.
Using a community plugin
defmodule MyAppWeb.Admin do
use PhoenixFilament.Panel, path: "/admin"
plugins do
plugin MyApp.AnalyticsPlugin
plugin MyApp.AuditLogPlugin, nav_group: "System"
end
endPlugin options (the keyword list after the module name) are passed to the plugin's
register/2 callback.
Creating a simple plugin
defmodule MyApp.AnalyticsPlugin do
use PhoenixFilament.Plugin
@impl true
def register(_panel, opts) do
%{
nav_items: [
nav_item("Analytics",
path: "/analytics",
icon: "hero-chart-bar",
nav_group: opts[:nav_group] || "Reports")
],
routes: [
route("/analytics", MyAppWeb.AnalyticsLive, :index)
]
}
end
endSee the Plugin Development Guide for the full plugin API.
Authorization
You can define an authorize/3 callback on any resource to control CRUD access:
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo
def authorize(:delete, _record, %{role: "admin"}), do: :ok
def authorize(:delete, _record, _user), do: {:error, :forbidden}
def authorize(_action, _record, _user), do: :ok
endauthorize/3 receives:
action—:index,:create,:edit,:delete, or:showrecord— the Ecto struct (ornilfor:create)user— the value ofsocket.assigns.current_user
Return :ok to allow or {:error, reason} to deny (raises UnauthorizedError).
Next Steps
- Resource Customization — Complete reference for form fields, table columns, filters, and authorization
- Plugin Development — Build and distribute your own plugins
- Theming Guide — Custom themes, CSS variables, brand customization
- API Reference — Module-level API documentation