FYI: this is a work in progress and it not complete as of yet.
In this post we are going to start with a new phoenix site example. We are going to assume you know how to spin up a phx.new app and generate the auth system using the mix tasks. If you need help doing that feel free to review how I do so here. https://morphic.pro/posts/a-start-to-developing-a-phoenix-application
Auth links
Lets start by cleaning up a layout. This is the nav the auth generator created for us.
lib/foobar_web/components/layouts/app.html.heex
Look for a ul
element just after the body
tag.
<ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<%= if @current_user do %>
<li class="text-[0.8125rem] leading-6 text-zinc-900">
<%= @current_user.email %>
</li>
<li>
<.link
href={~p"/users/settings"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Settings
</.link>
</li>
<li>
<.link
href={~p"/users/log_out"}
method="delete"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Log out
</.link>
</li>
<% else %>
<li>
<.link
href={~p"/users/register"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Register
</.link>
</li>
<li>
<.link
href={~p"/users/log_in"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Log in
</.link>
</li>
<% end %>
</ul>
Header
Lets add better syntax for our html to aid with ADA (Americans with Disabilities Act) and other software that will likely also use our website.
Instead of using a plain ul
for our main site we should start with a <header>
tag. Given our tag is a direct sibling of the body
like our ul
is currently it will also
have an implicit role of banner.
A banner landmark role overwrites the implicit ARIA role of the container element upon which it is applied. It should be reserved for globally repeating site-wide content that is generally located at the top of every page.
With out header
tag we need to position to the top of our site. We will add a few classes to this element to aid with that using the built in tailwind css library that came with our phoenix application.
<header class="absolute inset-x-0 top-0 z-10">
...
</header>
Lets breakdown what we just added and why. First we added the absolute class
Use the absolute utility to position an element outside of the normal flow of the document, causing neighboring elements to act as if the element doesn’t exist.
Then we added inset-x-0 and top-0
Use the top-, right-, bottom-, left-, and inset-* utilities to set the horizontal or vertical position of a positioned element. This affixes our
header
element to the top and full width of our screen.
Lastly we added z-10
Utilities for controlling the stack order of an element.
Nav
With our header
tag in place lets add our <nav>
tag
The <nav> HTML element represents a section of a page whose purpose is to provide navigation links, either within the current document or to other documents. Common examples of navigation sections are menus, tables of contents, and indexes.
Keep in mind we want to hang on to our links the auth generator created for us but for now we we should have something like this
<header class="absolute inset-x-0 top-0 z-10">
<nav class="flex items-center justify-between p-6" aria-label="Global">
<ul ...>
...
</ul>
</nav>
</header>
Branding
Now its time to add a home/root link. Inside our <nav>
add the following for our home page link.
<div class="flex lg:flex-1">
<a href="/" class="text-sm/6 font-semibold">
Foo Bar
</a>
</div>
In our first element of our <nav>
and we add an anchor tag that points to our root directory /
,
but we also add to classes that are used in it’s parent <div class="flex lg:flex-1">
We first default to using flex and than for larger screen sizes at the break point of lg
we say to use the flex-1
.
flex-1 == flex: 1 1 0%;
This will basically grow the width of automatically expand to fill available space.
Mobile nav button
Now its time to add a button that will only show for mobile screens sizes that we will use to toggle the menu
<div class="flex lg:hidden">
<button>
<span class="sr-only">Open Nav</span>
<.icon name="hero-bars-3" class="h-6 w-6" />
</button>
</div>
Again we use flex but also add a lg
break to add the hidden
class so that this is only viewable up to the lg
break point.
Lg screen nav
Now we can add some links for an example
<div class="hidden lg:flex lg:gap-x-12">
<a href="#" class="text-sm/6 font-semibold">Foo</a>
<a href="#" class="text-sm/6 font-semibold">Bar</a>
<a href="#" class="text-sm/6 font-semibold">Baz</a>
<a href="#" class="text-sm/6 font-semibold">Pop</a>
</div>
We see that we default to hidden at the smallest screen size and then set flex
at the large break point.
We also set a gap on the x dimension using a unit of 12.
gap-x-12 == column-gap: 3rem; /* 48px */
Clean up App layout
You should also note there is a nav in the upper layout inside this file.
lib/foobar_web/components/layouts/app.html.heex
file.
Go a head and remove the following.
<header class="px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
<div class="flex items-center gap-4">
<a href="/">
<img src={~p"/images/logo.svg"} width="36" />
</a>
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
v<%= Application.spec(:phoenix, :vsn) %>
</p>
</div>
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
@elixirphoenix
</a>
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
GitHub
</a>
<a
href="https://hexdocs.pm/phoenix/overview.html"
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
>
Get Started <span aria-hidden="true">→</span>
</a>
</div>
</div>
</header>
Auth links
Now we can start to add our auth links back to our nav. Before we do lets create a container that will keep our auth links to the right of the nav.
<div :if={@current_user} class="hidden lg:flex lg:flex-1 lg:gap-x-12 lg:justify-end">
<%= @current_user.email %>
<.link href={~p"/users/settings"} class="text-sm/6 font-semibold">
Settings
</.link>
<.link href={~p"/users/log_out"} method="delete" class="text-sm/6 font-semibold">
Log out
</.link>
</div>
Here we start to use some phoenix specific syntax used in heex
files. We add a if condition via the :if={}
attribute. We then use @current_user
as a truthy condition so that given the value is present and not nil
this div will then show.
Just like we have before we also see a few of the classes we have already used before for hidden and flex / flex-1, but this time we also add a lg:justify-end
to push our nav all the way to the right when its visible at the lg
break point.
We do the same thing for our non logged in users by adding another div with a condition but this time we invert the condition to say truthy if nil
via the bang or !
value before the @current_user
and show our links give you are not logged in.
<div :if={!@current_user} class="hidden lg:flex lg:flex-1 lg:gap-x-12 lg:justify-end">
<.link href={~p"/users/register"} class="text-sm/6 font-semibold">
Register
</.link>
<.link href={~p"/users/log_in"} class="text-sm/6 font-semibold">
Log in
</.link>
</div>
Review nav
For the most part our nav is complete. Lets have a look at the whole block of code as it stands now.
<header class="absolute inset-x-0 top-0 z-10">
<nav class="flex items-center justify-between p-6" aria-label="Global">
<div class="flex lg:flex-1">
<a href="/" class="text-sm/6 font-semibold">
Foo Bar
</a>
</div>
<div class="flex lg:hidden">
<button type="button">
<span class="sr-only">Open Nav</span>
<.icon name="hero-bars-3" class="h-6 w-6" />
</button>
</div>
<div class="hidden lg:flex lg:gap-x-12">
<a href="#" class="text-sm/6 font-semibold">Foo</a>
<a href="#" class="text-sm/6 font-semibold">Bar</a>
<a href="#" class="text-sm/6 font-semibold">Baz</a>
<a href="#" class="text-sm/6 font-semibold">Pop</a>
</div>
<div :if={@current_user} class="hidden lg:flex lg:flex-1 lg:gap-x-12 lg:justify-end">
<%= @current_user.email %>
<.link href={~p"/users/settings"} class="text-sm/6 font-semibold">
Settings
</.link>
<.link href={~p"/users/log_out"} method="delete" class="text-sm/6 font-semibold">
Log out
</.link>
</div>
<div :if={!@current_user} class="hidden lg:flex lg:flex-1 lg:gap-x-12 lg:justify-end">
<.link href={~p"/users/register"} class="text-sm/6 font-semibold">
Register
</.link>
<.link href={~p"/users/log_in"} class="text-sm/6 font-semibold">
Log in
</.link>
</div>
</nav>
</header>
Here what our header nav as of now should look like.
Mobile drop down menu
The only thing left now is to make our mobile button toggle a menu that will provide a way to show us our links when on a smaller screen.
Before we add logic for toggling the mobile menu we need a menu to show or hide.
<div
id="mobile-nav"
class="opacity-0 hidden lg:hidden fixed inset-0 z-20 bg-zinc-100 w-full h-screen"
>
<div class="fixed inset-y-0 left-0 z-20 w-full p-6">
<div class="flex items-center justify-between">
<a href="#" class="text-sm/6 font-semibold">
Foo Bar
</a>
<button type="button" class="rounded-md">
<span class="sr-only">Close menu</span>
<.icon name="hero-x-mark" class="h-6 h-6 text-zinc-800" />
</button>
</div>
<div id="mobile-nav-links" class="opacity-0 hidden -translate-y-10 mt-6 flow-root">
<div class="-my-6 divide-y divide-zinc-500/10">
<div class="space-y-2 py-6"></div>
<div :if={!@current_user} class="py-6">
<.link
href={~p"/users/register"}
class="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-zinc-800 hover:bg-zinc-50"
>
Register
</.link>
<.link
href={~p"/users/log_in"}
class="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-zinc-800 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-800"
>
Log in
</.link>
</div>
<div :if={@current_user} class="py-6">
<span class="text-[0.8125rem] leading-6 text-zinc-800 dark:text-zinc-200 font-semibold hover:text-zinc-700">
<%= @current_user.email %>
</span>
<.link
href={~p"/users/settings"}
class="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-zinc-800 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-800"
>
Settings
</.link>
<.link
href={~p"/users/log_out"}
method="delete"
class="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-zinc-800 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-800"
>
Log out
</.link>
</div>
</div>
</div>
</div>
</div>
Toggle logic
def show_mobile_nav(js \\ %JS{}) do
JS.show(js,
to: "#mobile-nav",
transition: {"transition-all transform ease-in duration-200", "opacity-0", "opacity-100"}
)
|> JS.show(
to: "#mobile-nav-links",
time: 300,
transition:
{"transition-all transform linear duration-300", "opacity-0 -translate-y-10",
"opacity-100 translate-y-0"}
)
end
def hide_mobile_nav(js \\ %JS{}) do
JS.hide(js,
to: "#mobile-nav",
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
)
|> JS.hide(
to: "#mobile-nav-links",
time: 300,
transition:
{"transition-all transform linear duration-300", "opacity-100 translate-y-0",
"opacity-0 -translate-y-10"}
)
end