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)
Hide visible elements with optional transitions:<button phx-click={JS.hide(to: "#modal", transition: "fade-out")}>
Hide Modal
</button>
Options:
:to - DOM selector
:transition - CSS classes or 3-tuple
:time - Duration in ms (default: 200)
:blocking - Block UI (default: true)
Toggle visibility based on current state:<button phx-click={JS.toggle(to: "#modal", in: "fade-in", out: "fade-out")}>
Toggle Modal
</button>
Options:
:to - DOM selector
:in - CSS classes for showing
:out - CSS classes for hiding
:time - Duration in ms (default: 200)
:display - Display value (default: "block")
:blocking - Block UI (default: true)
CSS Classes
add_class
remove_class
toggle_class
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)
Remove CSS classes from elements:<button phx-click={JS.remove_class("highlight underline", to: "#item")}>
Remove Highlight
</button>
Add or remove classes based on presence:<button phx-click={JS.toggle_class("active", to: "#item")}>
Toggle Active
</button>
Attributes
set_attribute
remove_attribute
toggle_attribute
ignore_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.
Remove an attribute from elements:<button phx-click={JS.remove_attribute("aria-expanded", to: "#dropdown")}>
Collapse
</button>
Toggle between two attribute values:<button phx-click={JS.toggle_attribute({"aria-expanded", "true", "false"}, to: "#dropdown")}>
Toggle
</button>
Mark attributes to skip during DOM patching:<dialog phx-mounted={JS.ignore_attributes("open")}>
<!-- open attribute won't be patched by server -->
</dialog>
Accepts single attribute, list, or wildcards like "data-*".
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
focus
focus_first
push_focus
pop_focus
Send focus to an element:JS.focus(to: "#search-input")
Focus first focusable child:JS.focus_first(to: "#modal")
Save current focus to restore later:JS.push_focus(to: "#my-button")
Restore previously pushed focus:
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.
Navigation
Navigate with pushState history:JS.navigate("/my-path")
JS.navigate("/my-path", replace: true)
Patch with pushState history:JS.patch("/my-path")
JS.patch("/my-path", replace: true)
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>
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:
String
Scoped: inner
Scoped: closest
Standard CSS selector:JS.show(to: "#modal")
JS.hide(to: ".notification")
JS.add_class("active", to: "body a:nth-child(2)")
Target elements within the interacted element:<div phx-click={JS.show(to: {:inner, ".menu"})}>
<div>Open me</div>
<div class="menu hidden" phx-click-away={JS.hide()}>
Dropdown content
</div>
</div>
Target closest ancestor element:JS.hide(to: {:closest, ".card"})
Modal Component Example
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