This guide covers the fundamentals of using LiveSvelte: the <.svelte> component, props, events, and the ~V sigil.
Your First Component
1. Create a Svelte component
Place Svelte files in assets/svelte/. LiveSvelte discovers all *.svelte files in that directory at compile time.
<!-- assets/svelte/Counter.svelte -->
<script>
let { count, live } = $props()
function increment() {
live.pushEvent("increment", {})
}
</script>
<div>
<p>Count: {count}</p>
<button onclick={increment}>Increment</button>
</div>2. Use it in a LiveView
# lib/my_app_web/live/counter_live.ex
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
def render(assigns) do
~H"""
<.svelte name="Counter" props={%{count: @count}} socket={@socket} />
"""
end
endThat's it. When the user clicks the button, pushEvent("increment", {}) sends the event to handle_event/3, the count is incremented, and Svelte re-renders automatically.
Props
Pass any JSON-serializable map as props:
<.svelte name="UserCard" props={%{name: @user.name, role: @user.role}} socket={@socket} />In the component, receive with $props():
<script>
let { name, role } = $props()
</script>
<div>
<h2>{name}</h2>
<span>{role}</span>
</div>Struct Props
Structs must implement the LiveSvelte.Encoder protocol before being passed as props. Use @derive for the default implementation:
defmodule MyApp.User do
@derive {LiveSvelte.Encoder, only: [:id, :name, :email]}
defstruct [:id, :name, :email, :password_hash]
endThe only: list controls which fields are exposed. Never derive without only: for structs with sensitive fields.
The live Prop
LiveSvelte automatically passes a live prop to every mounted component. Use it to communicate with the server:
<script>
let { live } = $props()
// Push event to server (fire-and-forget)
function save(data) {
live.pushEvent("save", data)
}
// Push event and receive reply
function saveWithReply(data) {
live.pushEvent("save", data, (reply) => {
console.log("Server replied:", reply)
})
}
// Subscribe to server-sent events
live.handleEvent("flash", ({ message }) => {
alert(message)
})
</script>Composable Alternative to live Prop
Instead of using the live prop, you can use composables which work from any component in the tree — no prop drilling:
<script>
import { useLiveSvelte, useLiveEvent } from "live_svelte"
const { pushEvent } = useLiveSvelte()
useLiveEvent("flash", ({ message }) => {
alert(message)
})
function save(data) {
pushEvent("save", data)
}
</script>See the API Reference for all composables.
Component Shorthand with LiveSvelte.Components
Add use LiveSvelte.Components to your LiveView (or web module) for shorthand component functions:
# In web module html_helpers (added by Igniter installer):
import LiveSvelte
use LiveSvelte.ComponentsThen instead of <.svelte name="Counter" ...>, use:
<.Counter count={@count} socket={@socket} />The function names are generated from your .svelte filenames. Counter.svelte → <.Counter>, UserCard.svelte → <.UserCard>.
socket is required for SSR
Always pass socket={@socket} when SSR is enabled. It's used to detect the initial dead render vs. connected live render. You can omit it only when ssr={false}.
Inline Templates with the ~V Sigil
For small, one-off components, write Svelte templates inline using the ~V sigil:
def render(assigns) do
~V"""
<script>
let { count } = $props()
</script>
<p>Count is {count}</p>
"""
endThe sigil writes the template to assets/svelte/_build/ at compile time and mounts it like any other component. All LiveView assigns are automatically available as props.
Svelte 5 Syntax Required
Always use Svelte 5 runes syntax. Do NOT use Svelte 4 patterns:
| ❌ Svelte 4 | ✅ Svelte 5 |
|---|---|
export let count | let { count } = $props() |
let x = 0 (reactive) | let x = $state(0) |
$: doubled = x * 2 | let doubled = $derived(x * 2) |
<script context="module"> | module-level code in .js files |
Local State
Local component state uses $state():
<script>
let { items } = $props()
let filter = $state("")
let filtered = $derived(items.filter(i => i.name.includes(filter)))
</script>
<input bind:value={filter} placeholder="Filter..." />
{#each filtered as item}
<li>{item.name}</li>
{/each}Component Discovery
LiveSvelte scans assets/svelte/**/*.svelte at compile time. Component names in <.svelte name="..."> are relative paths without the .svelte extension:
assets/svelte/Counter.svelte → name="Counter"
assets/svelte/forms/UserForm.svelte → name="forms/UserForm"phx-update="ignore"
LiveSvelte automatically sets phx-update="ignore" on the component wrapper div, which prevents LiveView from patching Svelte's DOM after mount. All updates flow through the hook. This is required for correct operation — do not override it.