Subscribe to access all episodes. View plans →

#197: LiveView Colocated Hooks

Published August 11, 2025

Colocated Hook Docs

Colocated JS Docs

Follow along with the episode starter on GitHub


In this episode, let’s look at one of my favorite new features that was included with Phoenix LiveView 1.1 - colocated JavaScript Hooks.

So, what are colocated hooks?

A colocated hook lives alongside your component code. This makes writing simple hooks much easier. Instead of having to create a separate file, we can just add our hook to our HEEx component. It’s a great feature when you need to add a bit of JavaScript to your LiveView components.

Let’s see how we can add a colocated hook to an existing Elixir Phoenix LiveView application. We have this album page here, and we want to add a button that copies the album link to your clipboard. We’ll create a hook to do that. Before we can write a colocated hook, we need to open our app.js file, and the first thing we need to do is tell our Phoenix app that we want to use colocated hooks by importing them from the phoenix-colocated folder. A new Phoenix 1.8 should have this all set up for you, but if you’re upgrading from an older application, you’ll need to add these lines.

# assets/js/app.js

...
import {hooks as colocatedHooks} from "phoenix-colocated/teacher"
...
const liveSocket = new LiveSocket("/live", Socket, {
  ...
  hooks: {...colocatedHooks},
})
...

With that setup, let’s add the copy link button. I’m going to open up our AlbumLive.Show module and add a button that users can click to copy the album link.

Phoenix hooks need a unique ID, so let’s set ours to id= "copy-album-link" and then we’ll add the phx-hook=".CopyLink". You may have noticed that I’m prefixing the name with a . - this is used to automatically prefix the colocated hook with the module name to avoid conflicts with other hooks.

Then I’ll add some styling. Now I’ll paste in the SVG we’ll use for the copy icon, and then here’s some hidden text “Copied” that we’ll display to give the user feedback when the URL has been copied.

# lib/teacher_web/live/album_live/show.ex

...
<button
  type= "button"
  id="copy-album-link"
  phx-hook=".CopyLink"
  class="inline-flex items-center ml-2 p-1 hover:cursor-pointer"
  aria-label= "Copy link"
>
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-link-45deg" viewBox="0 0 16 16">
    <path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/>
    <path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>
  </svg>
  <span class="ml-1 text-xs hidden">Copied</span>
</button>
...

Now here’s where the fun happens. In the same file, we’re going to add the JavaScript that makes this button actually do something.

We’ll scroll down and add a <script> tag with the “type” Phoenix.LiveView.ColocatedHook

And we need to give it the same name that we used for our hook .CopyLink.

Then I’ll paste in the code for our hook.

# lib/teacher_web/live/album_live/show.ex

...
<script :type={Phoenix.LiveView.ColocatedHook} name=".CopyLink">
  export default {
    mounted() {
      this.el.addEventListener("click", () => {
        navigator.clipboard.writeText(window.location.href);

        // Show "Copied" text
        const span = this.el.querySelector("span");
        span.classList.remove("hidden");

        // Hide after 2 seconds
        setTimeout(() => {
          span.classList.add("hidden");
        }, 2000);
      });
    }
  };
</script>

The mounted() function runs when our button appears on the page.

Inside, we’re setting up a click event listener. When someone clicks the button, we use the browser’s clipboard API to copy the current page URL. Then we find that hidden “Copied” text, show it for 2 seconds as feedback, then hide it again.

With those changes, let’s go back to the browser, and if we see our icon now, if we click it - Great! The “Copied” text appears, and if we open a new tab, we can paste in that same URL. Our colocated hook is working!

Not only can you write colocated hooks, but you can now write colocated JS. Let’s see how it works. We’ll add some colocated JS that will toggle our summaries to display the full summary for an album.

Back in our LiveView, we can see we have the preview summary that’s displayed and the hidden full summary.

Let’s add another button below the preview summary, and we’ll add a phx-click with a JS.dispatch call with the event “summary:toggle” that our JavaScript will listen for.

We’ll add some more styling and then the button text “Full summary”. Now let’s go ahead and copy this button and paste it below our full summary, updating the button text to read “Show less”.

# lib/teacher_web/live/album_live/show.ex

...
<p id="summary-preview" class="leading-relaxed">
  {String.slice(@album.summary, 0, 100)}...
  <button
    type= "button"
    phx-click={JS.dispatch("summary:toggle")}
    class= "text-blue-600 hover:underline hover:cursor-pointer"
  >
    Full summary
  </button>
</p>
<p id="summary-full" class="leading-relaxed hidden">
  {@album.summary}
  <button
    type= "button"
    phx-click={JS.dispatch("summary:toggle")}
    class= "text-blue-600 hover:underline hover:cursor-pointer"
  >
    Show less
  </button>
</p>
...

With those added, we just need to write the JavaScript that listens for this event.

# lib/teacher_web/live/album_live/show.ex

...
<script :type={Phoenix.LiveView.ColocatedJS}>
  window.addEventListener("summary:toggle", () => {
    document.getElementById("summary-preview").classList.toggle("hidden");
    document.getElementById("summary-full").classList.toggle("hidden");
  });
</script>
...

We’ll add another <script> tag -only this time we need to set the type to Phoenix.LiveView.ColocatedJS.

I’ll paste in our JavaScript. All we’re doing here is listening for the “summary:toggle” event, and when we hear it, we’re toggling the hidden class that’s on both summary paragraphs.

Let’s go back to the browser, and if we click on “Full summary,” there’s a bug - nothing happens. Let’s look at the browser console, and there’s our error - it can’t find our id= "summary-preview".

So let’s go back to our component, and here’s our issue: we have a typo in “summary-preview”. Let’s fix that.

Then, if we go back to the browser and try again. Great! We can toggle the album summary!

With Phoenix LiveView’s introduction of colocated Hooks and colocated JavaScript, it’s now much easier to write simple JS code.

© 2024 HEXMONSTER LLC