Documentation Index
Fetch the complete documentation index at: https://mintlify.com/phoenixframework/phoenix_live_view/llms.txt
Use this file to discover all available pages before exploring further.
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