View Source Svelte
This guide walks through configuring a Phoenix + Inertia.js app to render Svelte pages, bundled with esbuild.
A complete, runnable version of the manual setup below lives in
examples/esbuild/svelte.
Scope
The bulk of this guide sets up client-side rendering. Server-side rendering is covered at the end.
Why Svelte needs more than the esbuild CLI
React components written as JS/JSX are the easy case: esbuild bundles them as-is,
so the esbuild Hex package —
which runs the esbuild command-line tool — is all you need.
Svelte and Vue are different: their single-file components must be compiled to JavaScript first. For Svelte, that compilation runs as an esbuild plugin, and esbuild plugins are only available through esbuild's JavaScript API, never its CLI (evanw/esbuild#884).
That single constraint drives the whole setup: instead of the esbuild Hex
package, you install esbuild from npm and drive it from a small Node script. The
steps below remove the Hex package and wire Phoenix's watcher and asset aliases
to that script.
This is the esbuild path, not the canonical Svelte path
Svelte's ecosystem-standard tooling is now Vite
(@sveltejs/vite-plugin-svelte). This guide keeps you on Phoenix's default
esbuild pipeline using the community esbuild-svelte plugin. If you'd
rather adopt the canonical Svelte toolchain, set up Vite instead.
Install with Igniter
The Igniter installer performs every step below for you — including the server-side wiring — in one command:
mix inertia.install --client-framework svelte
Pass --typescript to set up TypeScript as well. The rest of this guide
documents the same setup by hand.
Manual setup
These steps assume the server-side adapter is already installed (the
Inertia.Plug, the Inertia.Controller / Inertia.HTML imports, and
config :inertia) per the Installation section.
1. Install the npm packages
From your app's assets directory:
npm install svelte @inertiajs/svelte esbuild esbuild-svelte
svelte— the framework and its compiler.@inertiajs/svelte— the Inertia client adapter for Svelte.esbuild— the bundler, now as a Node dependency.esbuild-svelte— the plugin that compiles.sveltefiles during bundling.
TypeScript
Type-only TypeScript in <script lang="ts"> (type annotations, interfaces,
import type) works out of the box — esbuild strips the types during the
build. Add typescript if you want editor support and type-checking.
You only need svelte-preprocess for TypeScript features that require real
transpilation, such as enums. To add it, install svelte-preprocess, then
pass preprocess: sveltePreprocess() to the plugin in step 2.
2. Add the esbuild build script
Create assets/esbuild.config.js:
// assets/esbuild.config.js
const esbuild = require("esbuild");
const sveltePlugin = require("esbuild-svelte");
const args = process.argv.slice(2);
const watch = args.includes("--watch");
const deploy = args.includes("--deploy");
const options = {
entryPoints: ["js/app.js"],
bundle: true,
format: "esm",
splitting: true,
chunkNames: "chunks/[name]-[hash]",
outdir: "../priv/static/assets/js",
logLevel: "info",
target: "es2022",
external: ["/fonts/*", "/images/*"],
minify: deploy,
sourcemap: watch ? "inline" : false,
// Required so esbuild resolves Svelte's `svelte` export condition.
conditions: ["svelte", "browser"],
mainFields: ["svelte", "browser", "module", "main"],
plugins: [
sveltePlugin({
// Inject component CSS through JS instead of emitting separate .css files.
compilerOptions: { css: "injected", dev: !deploy },
}),
],
};
async function run() {
if (watch) {
const ctx = await esbuild.context(options);
await ctx.watch();
console.log("esbuild: watching for changes...");
} else {
await esbuild.build(options);
}
}
run().catch((error) => {
console.error(error);
process.exit(1);
});Two options in here are easy to miss and will silently break your build if omitted:
conditions: ["svelte", ...](andmainFields) —@inertiajs/svelte'spackage.jsononly exposes asvelteexport condition. Without these, esbuild cannot resolve the package at all.compilerOptions: { css: "injected" }— With code splitting enabled, esbuild emits a separate.cssfile for each lazily-imported page chunk, and ESM dynamic imports never load those sibling stylesheets, so your page styles silently disappear. Injecting the CSS through JS keeps styling correct for lazily-loaded pages and means your root layout needs no extra<link rel="stylesheet">for component styles.
Content Security Policy
Injected CSS adds component styles via runtime <style> elements. Under a
strict CSP that disallows inline styles, you'll need a style-src nonce/hash
(or switch to esbuild's external CSS output and link it like the
Vue guide does).
3. Set up the Inertia entry point
Replace the contents of assets/js/app.js with the Inertia boot code:
// assets/js/app.js
import { createInertiaApp } from "@inertiajs/svelte";
import { mount } from "svelte";
createInertiaApp({
resolve: (name) => import(`./pages/${name}.svelte`),
setup({ el, App, props }) {
mount(App, { target: el, props });
},
http: {
// Phoenix expects the CSRF token via the `x-csrf-token` header. See the
// CSRF protection section of the README.
xsrfHeaderName: "x-csrf-token",
},
});The dynamic import of ./pages/${name}.svelte tells esbuild to bundle every
file under assets/js/pages into its own chunk, so a page name like "Home"
resolves to assets/js/pages/Home.svelte at runtime. mount is the Svelte 5
mounting API.
4. Remove the esbuild Hex package
Since esbuild now runs from Node, drop the Hex package and its configuration.
In mix.exs, remove the :esbuild dependency:
- {:esbuild, "~> 0.10", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev},In config/config.exs, remove the entire config :esbuild block:
- # Configure esbuild (the version is required)
- config :esbuild,
- version: "0.25.4",
- my_app: [
- args: ~w(js/app.js --bundle ...),
- cd: Path.expand("../assets", __DIR__),
- env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
- ]5. Point the dev watcher at Node
In config/dev.exs, replace the esbuild watcher with a node watcher that
runs the build script in watch mode:
watchers: [
- esbuild: {Esbuild, :install_and_run, [:my_app, ~w(--sourcemap=inline --watch)]},
+ node: ["esbuild.config.js", "--watch", cd: Path.expand("../assets", __DIR__)],
tailwind: {Tailwind, :install_and_run, [:my_app, ~w(--watch)]}
]6. Update the asset mix aliases
In mix.exs, point the assets.* aliases at the build script (and npm install)
instead of the esbuild tasks:
defp aliases do
[
setup: ["deps.get", "assets.setup", "assets.build"],
- "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
+ "assets.setup": ["tailwind.install --if-missing", "cmd --cd assets npm install"],
- "assets.build": ["compile", "tailwind my_app", "esbuild my_app"],
+ "assets.build": ["compile", "tailwind my_app", "cmd --cd assets node esbuild.config.js"],
"assets.deploy": [
"tailwind my_app --minify",
- "esbuild my_app --minify",
+ "cmd --cd assets node esbuild.config.js --deploy",
"phx.digest"
],
# ...
]
end(Replace my_app with your app's name.)
7. Load the bundle as an ES module
Code splitting produces ES modules, so the root layout must load the bundle with
type="module". In lib/my_app_web/components/layouts/root.html.heex, make sure
the <head> uses the Inertia components and a module script:
<.inertia_title>{assigns[:page_title]}</.inertia_title>
<.inertia_head content={@inertia_head} />
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
<script type="module" defer phx-track-static src={~p"/assets/js/app.js"}>
</script>8. Create a page and render it
Add a Svelte page at assets/js/pages/Home.svelte:
<script>
let { name } = $props();
</script>
<h1>Hello from {name}!</h1>Render it from a controller with render_inertia/2:
def home(conn, _params) do
conn
|> assign_prop(:name, "Svelte")
|> render_inertia("Home")
end9. Build and run
mix assets.build
mix phx.server
Visit your page and you should see the Svelte component rendered through Inertia.
In development, the node watcher rebuilds the bundle whenever you edit a
.svelte file.
Server-side rendering
With SSR, Phoenix pre-renders the initial page to HTML through a pool of Node
workers and the client hydrates it, instead of rendering into an empty
<div id="app">. Subsequent navigation stays client-side.
The steps below are the Svelte-specific pieces; enabling SSR itself — starting
the Inertia.SSR pool and setting config :inertia, ssr: true — is covered in
the README's Server-side rendering section.
A complete version lives in
examples/esbuild/svelte.
1. Add the SSR entry point
Create assets/js/ssr.js, which exports a render(page) function. It uses
Svelte 5's server render (aliased, since we also export a function named
render):
// assets/js/ssr.js
import { createInertiaApp } from "@inertiajs/svelte";
import { render as renderToHTML } from "svelte/server";
export function render(page) {
return createInertiaApp({
page,
resolve: (name) => import(`./pages/${name}.svelte`),
setup({ App, props }) {
return renderToHTML(App, { props });
},
});
}svelte/server is part of svelte, so there's nothing extra to install.
2. Build the SSR bundle
Extend assets/esbuild.config.js to also build ssr.js as a Node/CommonJS
module at priv/ssr.js. The SSR build targets Node and compiles components for
the server:
const ssr = {
...shared,
entryPoints: ["js/ssr.js"],
platform: "node",
format: "cjs",
outfile: "../priv/ssr.js",
conditions: ["svelte"],
mainFields: ["svelte", "module", "main"],
plugins: [
sveltePlugin({
// dev: false — Svelte 5's dev-mode server instrumentation errors during SSR.
compilerOptions: { generate: "server", css: "injected", dev: false },
}),
],
};
// build/watch both the client and ssr configsSee the example's esbuild.config.js
for the full file. Two things to note: generate: "server" is required (the
default compiles for the DOM), and dev: false avoids a Svelte 5 dev-mode SSR
crash. With injected CSS, the server render inlines component styles into the
head, so there's no separate ssr.css. Add priv/ssr.js to your
.gitignore.
3. Hydrate on the client
@inertiajs/svelte's createInertiaApp hydrates server-rendered markup and
mounts otherwise — automatically, as long as you don't pass a custom
setup. So drop the setup/mount from assets/js/app.js:
import { createInertiaApp } from "@inertiajs/svelte";
- import { mount } from "svelte";
createInertiaApp({
resolve: (name) => import(`./pages/${name}.svelte`),
- setup({ el, App, props }) {
- mount(App, { target: el, props });
- },
http: { xsrfHeaderName: "x-csrf-token" },
});4. Enable SSR
Start the Inertia.SSR pool and set config :inertia, ssr: true per the
README's Server-side rendering section.
Disable it in config/test.exs (config :inertia, ssr: false) so your test
suite doesn't need the Node pool.