Skip to main content
The Phoenix.LiveView.JS module provides commands for executing JavaScript utility operations on the client without writing custom JavaScript code.

Overview

JS commands are DOM-patch aware, meaning operations applied by the JS APIs persist across server updates. They support common client-side needs like:
  • Adding/removing CSS classes
  • Setting/removing attributes
  • Showing/hiding content
  • Animations and transitions
  • Pushing enhanced events to the server

Client Utility Commands

Visibility

Show hidden elements with optional transitions:
<div id="modal" class="hidden">
  My Modal
</div>

<button phx-click={JS.show(to: "#modal", transition: "fade-in")}>
  Show Modal
</button>
Options:
  • :to - DOM selector (default: interacted element)
  • :transition - CSS classes or 3-tuple for animation
  • :time - Transition duration in ms (default: 200)
  • :display - Display value (default: "block")
  • :blocking - Block UI during transition (default: true)

CSS Classes

Add CSS classes to elements:
<button phx-click={JS.add_class("highlight underline", to: "#item")}>
  Highlight
</button>
Options:
  • :to - DOM selector
  • :transition - Animation classes
  • :time - Duration in ms
  • :blocking - Block UI (default: true)

Attributes

Set an attribute on elements:
<button phx-click={JS.set_attribute({"aria-expanded", "true"}, to: "#dropdown")}>
  Expand
</button>
Cannot set DOM properties like input value. Use JS.dispatch/2 with custom events instead.

Transitions

Apply temporary CSS transitions:
<div id="item">My Item</div>
<button phx-click={JS.transition("shake", to: "#item")}>Shake!</button>
3-tuple syntax:
<div phx-mounted={JS.transition({"ease-out duration-300", "opacity-0", "opacity-100"}, time: 300)}>
  Fades in on mount
</div>
The time option should match the duration in your CSS classes for smooth animations.

Focus Management

Send focus to an element:
JS.focus(to: "#search-input")

Enhanced Push Events

Customize server event handling with additional options:
<button phx-click={JS.push("inc", 
                           loading: ".thermo", 
                           target: @myself, 
                           value: %{limit: 40})}>
  +
</button>

Options

  • :target - Selector or component ID (overrides phx-target)
  • :loading - Selector to apply loading classes to
  • :page_loading - Trigger page loading events (default: false)
  • :value - Map of values to send (overrides phx-value-* attributes)
Values from phx-value-* attributes are merged with the :value option, with the option taking precedence.

Dispatching Events

Dispatch custom DOM events:
<button phx-click={JS.dispatch("click", to: ".nav")}>Click Nav</button>

Options

  • :to - DOM selector (default: interacted element)
  • :detail - Map of data available in event.detail
  • :bubbles - Whether event bubbles (default: true)
  • :blocking - Block UI until event.detail.done() is called

Custom Event Example

window.addEventListener("app:clipcopy", (event) => {
  if ("clipboard" in navigator) {
    if (event.target.tagName === "INPUT") {
      navigator.clipboard.writeText(event.target.value)
    } else {
      navigator.clipboard.writeText(event.target.textContent)
    }
  }
})
click events dispatched with JS.dispatch are MouseEvent types and cannot have custom details.

Executing Stored Commands

Execute JS commands from element attributes:
<div id="modal" phx-remove={JS.hide("#modal")}>...</div>
<button phx-click={JS.exec("phx-remove", to: "#modal")}>Close</button>

Composing Commands

Commands can be chained together:
<button phx-click={
  JS.push("modal-closed") 
  |> JS.remove_class("show", to: "#modal", transition: "fade-out")
}>
  Hide Modal
</button>

Extracting to Functions

alias Phoenix.LiveView.JS

def hide_modal(js \\ %JS{}, selector) do
  js
  |> JS.push("modal-closed")
  |> JS.remove_class("show", to: selector, transition: "fade-out")
end
<button phx-click={hide_modal("#modal")}>Hide Modal</button>
Commands execute in order on the client. Fully client-side commands (like hide) execute immediately without waiting for server responses.

DOM Selectors

All commands accept a :to option with these selector types:
Standard CSS selector:
JS.show(to: "#modal")
JS.hide(to: ".notification")
JS.add_class("active", to: "body a:nth-child(2)")
Complete modal with show/hide functionality:
alias Phoenix.LiveView.JS

def hide_modal(js \\ %JS{}) do
  js
  |> JS.hide(transition: "fade-out", to: "#modal")
  |> JS.hide(transition: "fade-out-scale", to: "#modal-content")
end

def modal(assigns) do
  ~H"""
  <div id="modal" class="phx-modal" phx-remove={hide_modal()}>
    <div
      id="modal-content"
      class="phx-modal-content"
      phx-click-away={hide_modal()}
      phx-window-keydown={hide_modal()}
      phx-key="escape"
    >
      <button class="phx-modal-close" phx-click={hide_modal()}></button>
      <p>{@text}</p>
    </div>
  </div>
  """
end

Client-Side JS Execution

Execute JS commands from JavaScript:
// In a client hook
this.js().show(this.el, {transition: "fade-in"})

Encoding Commands

For custom JSON libraries or dynamic execution:
socket
|> push_event("myapp:exec_js", %{
  to: "#items-#{item.id}",
  js: JS.show() |> JS.to_encodable()
})
window.addEventListener("phx:myapp:exec_js", e => {
  const {to, js} = e.detail
  const el = document.querySelector(to)
  if (el && js) {
    window.liveSocket.execJS(el, js)
  }
})
When using Jason or JSON libraries, commands are automatically encoded—no need to call to_encodable/1.

See Also

Build docs developers (and LLMs) love