JS Interoperability

For a better development experience, you can configure Surface to automatically load JS hooks related to your components when a colocated .hooks.js file is provided.

This section assumes you already have basic knowledge of using JS hooks with LiveView. In case you don't, please read JavaScript interoperability for general guidance.


Update mix.exs, adding the :surface compiler to the list of compilers:

def project do
    compilers: [:phoenix] ++ Mix.compilers() ++ [:surface]

Update .gitignore:

# Ignore auto-generated hook files

Update assets/js/app.js:

import Hooks from "./_hooks"
// ...
let liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks, ... })

Update the Endpoint options in config/dev.exs for live reload of JS hooks:

config :my_app, MyAppWeb.Endpoint,
  reloadable_compilers: [:phoenix, :elixir, :surface],
  live_reload: [
    patterns: [


Create a colocated .hooks.js file with the same base name of the component.


├── ...
├── card.ex
├── card.hooks.js

Export the hooks you need in card.hooks.js:

let Card = {
    console.log("Card mounted")

let OtherHook = {
    console.log("OtherHook mounted")

export {Card, OtherHook};

Use the :hook directive to bind whatever hook you need:

<div :hook="Card">

You can also access hooks defined by other components using option :from:

<div :hook={{ "Card", from: MyComponents.CardList }}>

Choosing a different output dir

By default, Surface builds all components' JS files into ./assets/js/_hooks/. It you need the compiler use a different folder, you can set the hooks_output_dir configuration in your config/dev.exs. Example:

config :surface, :compiler, hooks_output_dir: "assets/js/surface"