Making a site nav using tailwind and phoenix JS

phoenix tailwind

FYI: this is a work in progress and it not complete as of yet.

alt text 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">&rarr;</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