symfony/ux-svelte
Official Symfony UX integration for Svelte. Render Svelte components from Twig, pass props, and add interactive UI to Symfony apps without building a separate SPA. Includes Stimulus bridge, asset setup, and conventions for modern Svelte frontends.
Prerequisites. This package is thin glue: Svelte itself, a bundler that can compile .svelte files, and Stimulus (via symfony/stimulus-bundle) for mounting. You also want symfony/webpack-encore-bundle or a Vite/esbuild setup — AssetMapper alone cannot compile Svelte, so plan for a real build step.
Minimal path:
composer require symfony/ux-svelte
npm install --save-dev svelte @symfony/ux-svelte
In webpack.config.js:
Encore.enableSvelteLoader()
.addEntry('app', './assets/app.js');
Confirm the Flex recipe added the controller to assets/controllers.json:
{
"controllers": {
"@symfony/ux-svelte": {
"svelte": { "enabled": true, "fetch": "eager" }
}
},
"entrypoints": []
}
Create assets/svelte/components/Hello.svelte:
<script>
export let name = 'world';
</script>
<p>Hello {name}!</p>
Register it (typically in assets/app.js or a dedicated assets/svelte/index.js):
import { registerSvelteControllerComponents } from '@symfony/ux-svelte';
import Hello from './svelte/components/Hello.svelte';
registerSvelteControllerComponents({ Hello });
Use it in Twig:
{{ svelte_component('Hello', { name: 'Symfony' }) }}
Where to look first. The README’s quickstart, then assets/controllers.json (to verify the controller is enabled), then the PHP controller at vendor/symfony/ux-svelte/src/SvelteComponent.php and the JS controller in vendor/symfony/ux-svelte/src/Resources/assets/src/. The JS source is the real API surface — the docs are terse and the test fixtures are the only trustworthy examples for prop/event behavior.
First realistic use case. Drop a self-contained interactive widget — a counter, a date picker, a small filter panel — into an existing server-rendered Twig page. You get Stimulus lifecycle management and prop passing without rewriting the page in Svelte.
Glob-register all components. Avoid a manually maintained registry:
import { registerSvelteControllerComponents } from '@symfony/ux-svelte';
const components = import.meta.glob('./svelte/components/*.svelte', { eager: true });
registerSvelteControllerComponents(
Object.fromEntries(
Object.entries(components).map(([path, mod]) => [
path.split('/').pop().replace('.svelte', ''),
mod.default,
])
)
);
Keep the name you pass to svelte_component() in sync with the key — mismatches fail at runtime, not build time.
Props from Twig. Anything JSON-serializable works. For complex shapes, build the object server-side (a DTO JsonSerializable, a controller-provided array) rather than hand-writing JSON in the template:
{{ svelte_component('Chart', {
series: chart.series,
title: chart.title
}) }}
Multiple instances. Each svelte_component() call renders an independent wrapper element, and Stimulus instantiates a fresh component per element. You can safely render the same component dozens of times in a loop; state is isolated per instance.
Communicating back to Symfony. Svelte’s createEventDispatcher events do not automatically bubble to the DOM element in a way Symfony/Stimulus listens to. Two reliable patterns:
CustomEvent on the host element yourself (element.dispatchEvent(new CustomEvent('foo', { detail, bubbles: true }))), then attach a Stimulus action or addEventListener on the wrapper.Wrapping with a Stimulus controller. For anything beyond a leaf widget, put your own Stimulus controller around the Svelte component. The Stimulus controller owns the element lifecycle, handles navigation/fetch, and forwards events — the Svelte component stays a pure view:
// assets/controllers/dashboard_controller.js
export default class extends Controller {
connect() {
this.element.addEventListener('svelte:submit', this.onSubmit);
}
disconnect() {
this.element.removeEventListener('svelte:submit', this.onSubmit);
}
onSubmit = (e) => { /* fetch, Turbo stream, etc. */ };
}
Turbo integration. Components mount on connect and unmount on disconnect, so Turbo frame swaps and page visits re-create instances correctly. Treat component state as ephemeral — it will be destroyed on navigation. For persistent UI state, lift it into a Stimulus controller or a server-side store.
Splitting bundles. Large components are best lazy-registered. fetch: "lazy" in controllers.json only defers the Stimulus controller, not your component chunk; use dynamic import() in your own loader if bundle size matters, or split by Encore entry per page section.
TypeScript. The package itself is written in TS and ships types, but your .svelte files need svelte-check plus a svelte-preprocess/tsconfig setup in the loader. Encore’s enableSvelteLoader() accepts options if you need custom preprocessors.
AssetMapper is not enough. This is the single most common onboarding failure. Svelte requires a compiler; if you’ve migrated to AssetMapper-only, you still need a separate esbuild/Vite step for .svelte files, then import the output. There is no runtime Svelte loader in the browser.
Svelte 4 vs. Svelte 5. Svelte 5 replaced new Component({...}) with mount(Component, { target, props }) and introduced runes ($state, $props). Older releases of @symfony/ux-svelte assumed Svelte 3/4 semantics; Svelte 5 support depends on the package version. Pin both svelte and @symfony/ux-svelte together, and check the package’s peerDependencies before upgrading either. A mismatch typically surfaces as “component is not a constructor” or a silently empty mount.
Prop serialization is strict. Props are JSON-encoded into a data attribute. Functions, undefined, Date instances (serialize to strings), BigInt, and circular references break. Normalize dates to ISO strings server-side and parse them in the component.
Prop names must match export let. Svelte 4 ignores unknown props silently; Svelte 5 warns. Typos in Twig keys produce blank components with no error — inspect the DOM element’s data-symfony--ux--svelte--svelte-props-value attribute to confirm what was actually passed.
Debugging the wrapper. The rendered element carries data-controller="symfony--ux--svelte--svelte" plus data-symfony--ux--svelte--svelte-component-value (and -props-value). When a component doesn’t render: (1) check the controller name matches what you registered, (2) confirm the controller is enabled in controllers.json, (3) look for a console error like “Unknown Svelte component” thrown from the registry lookup, (4) check that the entrypoint JS is actually loaded on the page.
No SSR / hydration. This package mounts client-side only. Server-rendered fallback content inside the svelte_component() element is not hydrated — it’s replaced. If SEO or no-JS support matters, render meaningful markup separately and consider the Svelte component as an enhancement layer.
Config path quirks. config/packages/ux_svelte.yaml exposes a components directory setting (used by the Flex recipe for the default path). If your components live outside assets/svelte/components, either update that setting or, more reliably, control resolution via your bundler’s resolve.extensions / aliases. Mismatched paths between config and webpack produce “module not found” at build time, not runtime.
Turbo cache restore. Turbo’s snapshot cache restores DOM from a clone; Stimulus re-connects, so Svelte remounts. Any state written directly into the DOM by the component (focus, scroll, open dropdowns) will be stale on back/forward navigation. Reset via connect().
Small ecosystem, read the source. With ~30 stars and a low adoption score, expect to solve edge cases yourself. There is little third-party documentation beyond the README. Treat the version as security-sensitive and pin it in package.json/composer.lock; the JS and PHP sides must be upgraded together because the data-attribute contract is the interface.
Extension points. The PHP SvelteComponent (Twig function) and the JS registerSvelteControllerComponents are the two seams. For anything unusual — passing a dispatch callback, per-instance options, wrapping in your own controller — the cleanest approach is to bypass svelte_component() and render the wrapper element yourself, mounting via your own Stimulus controller with mount()/new Component(). You keep Stimulus lifecycle management and sidestep the package’s assumptions about props and events.
Build performance. The Svelte compiler is fast, but Encore recompiles every .svelte file in the graph on change. For large component trees, use svelte-loader’s hotReload and scope entries per feature to keep rebuilds under a second.
How can I help you explore Laravel packages today?