Welcome to the web blog
Bit pusher at Spotify. Previously Interactive News at the New York Times, U.S. Digital Service, and Code for America.
Long time, no blog! Anyways, I’ve been messing around with Elixir and Phoenix for a side project around managing some chess tournaments. Since I plan on occasionally managing the application from my phone, I needed a way to handle collapsing navigation at a different viewport. I ended up using the classic hamburger menu implemented using Phoenix 1.7’s new components.
I have been enjoying working with Elixir and Phoenix, though is quite a different way of thinking about problems and how to organize code, and I am certainly no expert, but I liked this approach.
Here’s a short gif of the component dropped on top of a freshly generated
Phoenix application (generated via mix phx.new --no-ecto hamburger
):
And here is the full component’s code:
defmodule HamburgerWeb.Topnav do
import HamburgerWeb.CoreComponents, only: [icon: 1]
use Phoenix.Component
alias Phoenix.LiveView.JS
def nav(assigns) do
~H"""
<header class="px-4 z-40 sticky bg-white">
<div class="flex items-center justify-between border-b border-gray-200 text-sm py-3 z-40">
<p class="bg-brand/5 text-brand rounded-xl px-2 font-medium leading-6">
Hamburger
</p>
<.hamburger_button />
<.top_nav_content />
</div>
<.open_hamburger />
</header>
"""
end
defp top_nav_content(assigns) do
~H"""
<div class="hidden md:flex md:items-center md:gap-4 font-semibold leading-6">
<a href="/" class="hover:text-zinc-500">
Home
</a>
</div>
"""
end
defp hamburger_button(assigns) do
~H"""
<div class="md:hidden">
<button phx-click={show_hamburger()}>
<.icon name="hero-ellipsis-vertical-mini" />
</button>
</div>
"""
end
defp open_hamburger(assigns) do
~H"""
<div id="hamburger-container" class="hidden relative z-50">
<div id="hamburger-backdrop" class="fixed inset-0 bg-zinc-50/90 transition-opacity"></div>
<nav
id="hamburger-content"
class="fixed top-0 left-0 bottom-0 flex flex-col grow justify-between w-3/4 max-w-sm py-6 bg-white border-r overflow-y-auto"
>
<div>
<div class="flex items-center mb-4 place-content-between mx-4 border-b-zinc-200">
<div class="flex items-center gap-4">
<p class="bg-brand/5 text-brand rounded-xl px-2 font-medium leading-6">
Hamburger
</p>
</div>
<button class="navbar-close" phx-click={hide_hamburger()}>
<.icon name="hero-x-mark-mini" />
</button>
</div>
<div>
<ul></ul>
<li class="block px-6 py-2 text-sm font-semibold hover:bg-gray-200" phx-click="/">
<a href="/">Home</a>
</li>
</div>
</div>
</nav>
</div>
"""
end
defp show_hamburger(js \\ %JS{}) do
js
|> JS.show(
to: "#hamburger-content",
transition:
{"transition-all transform ease-in-out duration-300", "-translate-x-3/4", "translate-x-0"},
time: 300,
display: "flex"
)
|> JS.show(
to: "#hamburger-backdrop",
transition:
{"transition-all transform ease-in-out duration-300", "opacity-0", "opacity-100"}
)
|> JS.show(
to: "#hamburger-container",
time: 300
)
|> JS.add_class("overflow-hidden", to: "body")
end
defp hide_hamburger(js \\ %JS{}) do
js
|> JS.hide(
to: "#hamburger-backdrop",
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
)
|> JS.hide(
to: "#hamburger-content",
transition:
{"transition-all transform ease-in duration-200", "translate-x-0", "-translate-x-3/4"}
)
|> JS.hide(to: "#hamburger-container", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
end
end
Here’s how it works in a bit more detail: In the top navigation, there are three separate sections:
md:
breakpoint is set as (for more, see Tailwind media query
documentation). Phoenix
1.7 ships with Tailwind by default.display: hidden
at larger viewports and
visible at smaller ones.The hamburger button has a phx-click
listener on it. This is a nice addition
in Phoenix to allow for server-generated files to have some basic client-side
capabilities. When the button is clicked, the show_hamburger/1
method is
called. This method dispatches a handful of JS.show/1
events to
the different parts of the side navigation. The side navigation portion of the
hamburger menu is also made up of three components, each of which gets a
different show transition applied to them:
ease-in-out
transition.
The actual transition slides by translating the element from offscreen by the
inverse of its width to 0, meaning that it will slide in to the right until
the final left edge of the element is at position 0.opacity-0
(totally invisible) to opacity-100
(totally visible).The contents also contain a little x
icon, which triggers hiding the contents
by basically replaying the steps in reverse.
That’s the main portion of this component. It’s pretty straightforward to extend this by dropping additional pieces into slots.