Phoenix 1.7.0-rc.0 is out!

You can read the announcement on the blog, but I’ll dup most of it here for discussion purposes:


The first release candidate of Phoenix 1.7 is out! Phoenix 1.7 packs a number of long-awaited new features like verified routes, Tailwind support, LiveView authentication generators, unified HEEx templates, and more. This is a backwards compatible release with a few deprecations. Most folks should be able to update just by changing a couple dependencies.

Note: For the rc period, you’ll need to explicitly install the generator from hex to try out a fresh project:

mix archive.install hex phx_new 1.7.0-rc.0

Verified Routes

Verified routes replace router helpers with a sigil-based (~p), compile-time verified approach.

note: Verified routes make use of new Elixir 1.14 compiler features. Phoenix still supports older Elixir versions, but you’ll need to update to enjoy the new compile-time verification features.

In practice this means where before you used autogenerated functions like:

  # router
  get "/oauth/callbacks/:id", OAuthCallbackController, :new

  # usage
  MyRouter.Helpers.o_auth_callback_path(conn, :new, "github")
  # => "/oauth/callbacks/github"

  MyRouter.Helpers.o_auth_callback_url(conn, :new, "github")
  # => "http://localhost:4000/oauth/callbacks/github"

You can now do:

  # router
  get "/oauth/callbacks/:id", OAuthCallbackController, :new

  # usage
  # => "/oauth/callbacks/github"

  # => "http://localhost:4000/oauth/callbacks/github"

This has a number of advantages. There’s no longer guesswork on which function was inflected – is it Helpers.oauth_callback_path or o_auth_callback_path, etc. You also no longer need to include the %Plug.Conn{}, or %Phoenix.Socket{}, or endpoint module everywhere when 99% of the time you know which endpoint configuration should be used.

There is also now a 1:1 mapping between the routes you write in the router, and how you call them with ~p. You simply write it as if you’re hard-coding strings everywhere in your app – except you don’t have maintenance issues that come with hardcoding strings. We can get the best of both worlds with ease of use and maintenance because ~p is a compile-time verified against the routes in your router.

For example, imagine we typo a route:

<.link href={~p"/userz/profile"}>Profile</.link>

The compiler will dispatch all ~p’s at compile-time against your router, and let you know when it can’t find a matching route:

    warning: no route path for AppWeb.Router matches "/postz/#{post}"
      lib/app_web/live/post_live.ex:100: AppWeb.PostLive.render/1

Dynamic “named params” are also simply interpolated like a regular string, instead of arbitrary function arguments:


Additionally, interpolated ~p values are encoded via the Phoenix.Param protocol.
For example, a %Post{} struct in your application may derive the Phoenix.Param
protocol to generate slug-based paths rather than ID based ones. This allows you to
use ~p"/posts/#{post}" rather than ~p"/posts/#{post.slug}" throughout your

Query strings are also supported in verified routes, either in traditional query
string form:


Or as a keyword list or map of values:

params = %{page: 1, direction: "asc"}

Like path segments, query strings params are proper URL encoded and may be interpolated
directly into the ~p string.

Once you try out the new feature, you won’t be able to go back to router helpers. The new phx.gen.html|live|json|auth generators use verified routes.

Component-based Tailwind generators

Phoenix 1.7 ships with TailwindCSS by default, with no dependency on nodejs on the system. TailwindCSS is the best way I’ve found to style interfaces in my 20 years of web development. Its utility-first approach is far more maintainable and productive than any CSS system or framework I’ve used. It’s collocated approach also aligns perfectly within the fucntion component and LiveView landscape.

The Tailwind team also generously designed the new project landing page, CRUD pages, and authentication system pages for new projects, giving you a first-class and polished starting point for building out your apps.

A new project will contain a CoreComponents module, housing a core set of UI components like tables, modals, forms, and data lists. The suite of Phoenix generators (phx.gen.html|live|json|auth) make use of the core components. This has a number of neat advantages.

First, you can customize your core UI components to suit whatever needs, designs, and tastes that you have. If you want to use Bulma or Bootstrap instead of Tailwind – no problem! Simply replace the function definitions in core_components.ex with your framework/UI specific implementations and the generators continue to provide a great starting point for new features whether you’re a beginner, or seasoned expert building bespoke product features.

In practice, the generators give you templates that make use of your core components, which look like this:

  New Post
  <:subtitle>Use this form to manage post records in your database.</:subtitle>

<.simple_form :let={f} for={@changeset} action={~p"/posts"}>
  <.error :if={@changeset.action}>
    Oops, something went wrong! Please check the errors below.

  <input field={{f, :title}} type="text" label="Title" />
  <input field={{f, :views}} type="number" label="Views" />

    <.button>Save Post</.button>

<.back navigate={~p"/posts"}>Back to posts></.back>

We love what the Tailwind team designed for new applications, but we also can’t wait to see the community release their own drop-in replacements for core_components.ex for various frameworks of choice.

Unified function components across Controllers and LiveViews

Function components provided by HEEx, with declarative assigns and slots, are massive step-change in the way we write HTML in Phoenix projects. Function components provide UI building blocks, allowing features to be encapsulated and better extended over the previous template approach in Phoenix.View. You get a more natural way to write dynamic markup, reusable UI that can be extended by the caller, and compile-time features to make writing HTML-based applications a truly first-class experience.

Function components bring a new way to write HTML applications in Phoenix, with new sets of conventions. Additionally, users have struggled with how to marry controller-based Phoenix.View features with Phoenix.LiveView features in their applications. Users found themselves writing render("table", user: user) in controller-based templates, while their LiveViews made use of the new <.table rows={@users}> features. There was no great way to share the approaches in an application.

For these reasons, the Phoenix team unified the HTML rendering approaches whether from a controller request, or a LiveView. This shift also allowed us to revisit conventions and align with the LiveView approach of collocating templates and app code together.

New applications (and the phx generators), remove Phoenix.View as a dependency in favor of a new Phoenix.Template dependency, which uses function components as the basis for all rendering in the framework.

Your controllers still look the same:

defmodule AppWeb.UserController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    users = ...
    render(conn, :index, users: users)

But instead of the controller calling AppWeb.UserView.render("index.html", assigns), we’ll now first look for an index/1 function component on the view module, and call that for rendering if it exists. Additionally, we also renamed the inflected view module to look for AppWeb.UserHTML, or AppWeb.UserJSON, and so on for a view-per-format approach for rendering templates. This is all done in backwards compatible way, and is opt-in based on options to use Phoenix.Controller.

All HTML rendering is then based on function components, which can be written directly in a module, or embedded from an external file with the new embed_templates macro provided by Phoenix.Component. Your PageHTML module in a new application looks like this:

defmodule AppWeb.PageHTML do
  use AppWeb, :html

  embed_templates "page_html/*"

The new directory structure will look something like this:

├── controllers
│   ├── page_controller.ex
│   ├── page_html.ex
│   ├── error_html.ex
│   ├── error_json.ex
│   └── page_html
│       └── home.html.heex
├── live
│   ├── home_live.ex
├── components
│   ├── core_components.ex
│   ├── layouts.ex
│   └── layouts
│       ├── app.html.heex
│       └── root.html.heex
├── endpoint.ex
└── router.ex

Your controllers-based rendering or LiveView-based rendering now all share the same function components and layouts. Whether running phx.gen.html,, or phx.gen.auth, the new generated templates all make use of your components/core_components.ex definitions.

Additionally, we have collocated the view modules next to their controller files. This brings the same benefits of LiveView collocation – highly coupled files live together. Files that must change together now live together, whether writing LiveView or controller features.

These changes were all about a better way to write HTML-based applications, but they also simplified rendering other formats, like JSON. For example, JSON based view modules follow the same conventions – Phoenix will first look for an index/1 function when rendering the index template, before trying render/2. This allowed us to simplify JSON rendering in general and do away with concepts like Phoenix.View.render_one|render_many.

For example, this is a JSON view generated by phx.gen.json:

defmodule AppWeb.PostJSON do
  alias AppWeb.Blog.Post

  @doc """
  Renders a list of posts.
  def index(%{posts: posts}) do
    %{data: for(post <- posts, do: data(post))}

  @doc """
  Renders a single post.
  def show(%{post: post}) do
    %{data: data(post)}

  defp data(%Post{} = post) do
      title: post.title

Notice how it’s all simply regular Elixir functions – as it should be!

These features provide a unified rendering model for applications going forward with a new and improved way to write UIs, but they are a deviation from previous practices. Most large, established applications are probably best served by continuing to depend on Phoenix.View.

Alternative Webserver Support

Thanks to work by Mat Trudel, we now have the basis for first-class webserver support in Plug and Phoenix, allowing other webservers like Bandit to be swapped in Phoenix while enjoying all features like WebSockets, Channels, and LiveView. Stay tuned to the Bandit project if you’re interested in a pure Elixir HTTP server or give it a try in your own Phoenix projects!

As always, step-by-step upgrade guides are there to take your existing 1.6.x apps up to 1.7.

The full changelog can be found here.

Find us on elixir slack or the forums if you have issues.

Happy coding!



Congrats Chris! Looking forward to testing it in a couple of days
Exciting days for the Elixir community! :fire:


Whoo hoo! :048:

Congrats to Chris and everyone who worked on 1.7 - what an awesome release!

I did wonder whether this might be an issue - should have known you’d have already thought of it! :smiley:

This is great! I’ve been itching to move to a new css framework as Foundation has been showing its age for a while now - glad you made the decision for me!

That was really nice of them and honestly makes me want to use Tailwind more. Thank you to the Tailwind team for getting involved and for your support of Phoenix :orange_heart:

Btw if anyone is interested we will probably run a Modern CSS with Tailwind book club on DT soon - PM me if interested, I should be able to get you a copy!

Thanks for all these changes and updates - I love how refined LV feels now. For anyone who hasn’t read it, Chris’s recent blog post is well worth a read if you’re interested in where many of those ideas come from and why they’re such a great fit for LV.

I hope we’ll see the Programming LiveView book (and other learning resources) updated with these changes soon, I’m sure lots of people will be eager to get stuck in! :icon_cool:


Nice! It came even faster than I expected :smiley:

Ready to start some project with Phoenix!


Congratulations gang! Can’t wait to play around with it and see it firsthand.


template and view folders replaced by components - nice!

Love the use of attr and slot elements - clean.

Tailwind - to me the long list of class names look unnatural. Oh well I trust my framework authors and will give it a go. Will be interesting to compare/contrast when someone publishes a CoreComponent with bootstrap or milligram.

The input component uses classes like phx-no-feedback and phx-no-feedback:focus. Are these custom tailwind classes? Or maybe JS targets? Or something else??


Sorry for the meme, y’all, but…


They have always come predefined in a generated project. They used to use milligram IIRC. Generally, you updated the CSS for these classes to suit your design.



These are tailwind variants and you can check them in the tailwind.config.js file, whenever a form is in the process of submit you can use something like phx-submit-loading:bg-red-300 to style the button.



It looks amazing and am testing it right now to get my first live_view app running.

One question, where do you want the feedback for the RC? I am getting problems using scopes.

If I copy and paste these routes into the “/” scope it will work, but if I change them to a new scope to be able to introduce user management I get an error:

  scope "/texts", TextLive do
    pipe_through :browser

    live "/", Index, :index
    live "/new", Index, :new
    live "/:id/edit", Index, :edit

    live "/:id", Show, :show
    live "/:id/show/edit", Show, :edit
1 Like

I ported a bunch of the core_component.ex components to bulma a few weeks ago and tbh there’s barely a difference. Yes it may be a handful of classes instead of two (maybe three) handful, but on the other hand many UI frameworks can be customized easily only by ways built into the framework, while tailwind allows you to change whatever you need, given it’s all in the component.

In the end to me this is just the difference between a css framework (abstracting styling of content) vs. ui frameworks (providing pre-styled components), which I personally categorize bootstrap, bulma, milligram, … to be. Compared to the others Milligram just doesn’t do many custom components, but mostly html primitives.

It did however show how flexible the component system is as I hardly needed to adjust things in the layout or generated page templates.


Congratulations! This is massive! :clap:

My app is not using LV at the moment, but all the improvements to SSR and JSON APIs that I’m getting thanks to LV are amazing: first-class component support for classic views and clean JSON views. :1st_place_medal:


Tailwind is awesome. I like it when things that belong together, stay together. Also naming is hard so glad Tailwind takes care of it.

I just picked up the classes, without fumbling through css and I was able to change the styling of generated Auth UI.

I wish the UI generated by auth had the styling by default, but I don’t know how tough it is for generators to modify existing generated apps.

P.S. Generated UI is so Beautiful. I feel like a kid exploring changes in the new release.


Congrats @chrismccord and team :tada: :partying_face:


Will Phoenix Live Storybook get added to the release as well?


Love the verified routes / ~p sigil, but running into a compilation issue with dynamic paths for static assets:


Results in

path segments must begin with /, got: ".svg" in "/images/icons/#{icon}.svg"

Any pointers on how to work around this?


See the statics option here: Phoenix.VerifiedRoutes — Phoenix v1.7.0-rc.0


We’ve been working with @cblavier to marry up the declarative assigns work with the storybook as a starting point for eventual integration. BTW the story book is fantastic and can be used today! Longer term I would definitely like to ship it by default with apps, but we need to figure out a few odds and ends first. For example it would be great if the story book was pre-populated with the tailwind design system that we ship, but I haven’t been able to borrow any cycles yet to explore that. Meanwhile Christian has been hard at work, so stay tuned!


we can only dispatch against the router using segments split on “/”, so in your case something like this is what you want:

~p"/images/icons/#{icon <> ".svg"}"

Edit: we can probably be smarter about this kind of case, but will need a bit before I can look at it.