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
end

That'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]
end

The 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.Components

Then 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>
  """
end

The 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 countlet { count } = $props()
let x = 0 (reactive)let x = $state(0)
$: doubled = x * 2let 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.