symfony/ux-vue
Symfony UX Vue integrates Vue.js into Symfony apps, letting you render Vue components from Twig, pass props, and add interactive frontends while staying within the Symfony UX ecosystem, with straightforward asset and progressive-enhancement support.
Read this as a pattern reference, not a dependency. symfony/ux-vue is the Symfony UX bridge that mounts Vue components from server-rendered templates (Twig components like <twig:Vue component="Hello" props="{{ {...} }}"/>). Nothing in it is reusable inside a Laravel app — the bundle, Flex recipe, Twig extension and container tags are all Symfony-specific — but the client-side half of the design (glob-based component registration, serialized props, "islands" instead of a full SPA) is exactly what you'd hand-roll in Laravel.
Where to look first, in order:
src/VueComponentRenderer.php and the Twig template it renders — this is the server→client props contract.assets/src/ entrypoint — how it finds and mounts components by name.X.Y.Z placeholder — use your lockfile version, not the README's).Minimum viable experiment in a Laravel app, mirroring what the package does:
// resources/js/app.js
import { createApp } from 'vue'
const components = import.meta.glob('./components/*.vue', { eager: true })
document.querySelectorAll('[data-vue]').forEach((el) => {
const { component, props } = el.dataset
const definition = components[`./components/${component}.vue`]
createApp(definition.default, JSON.parse(props)).mount(el)
})
<div data-vue data-component="Hello" data-props="{{ json_encode(['name' => 'World']) }}"></div>
First real use case: replacing a jQuery/Blade sprinkle with a single Vue component on one page, without adopting Inertia. That's the same "progressive enhancement" niche the Symfony package fills.
Islands, not a root SPA. The package intentionally mounts one app per component instance rather than assuming a single #app. Steal this when you have a Blade-heavy app and only a couple of interactive widgets — it keeps you off Inertia and avoids fighting @vite + Blade layouts. Mount on [data-vue] nodes, pass only serializable props.
Props are JSON, treat them as an API. Everything crossing the boundary must survive json_encode — scalars, arrays, plain objects. Eloquent models should be mapped through an explicit array or a JsonResource, never passed raw. This is the same discipline as Inertia props, and the same failure mode (dates, Carbon, lazy-loaded relations) applies.
Auto-registration by convention. The import.meta.glob('./components/*.vue') lookup by kebab/Pascal name is the highest-value idea here. It removes the "did I forget to register the component?" class of bug and scales to dozens of components without a central registry.
Vite as the only bundler. The Symfony package has an adapter layer because it supports AssetMapper, Webpack Encore, and Vite. In Laravel you have exactly one path: laravel-vite-plugin + @vitejs/plugin-vue. Skip the abstraction; it's dead weight.
Testing. Vitest + @vue/test-utils on the component (mount with fixture props), and one PHP feature test that asserts the wrapper element renders with the expected data-props payload. That splits "props serialized correctly" from "component works" so a failure points at one side.
Hybrid / migration cases. If you're on a codebase that has both Symfony and Laravel (or you're writing a package that targets both), the renderer is the right seam: an interface with a Twig implementation and a Blade implementation, sharing the client-side registry.
composer require this in a Laravel app. The service definitions, symfony/stimulus-bundle dependency and Twig extension autoconfiguration will either error out or silently do nothing. Copy the ideas, not the package.symfony/asset-mapper 8.2+. Only relevant if you're on a pre-release Symfony 8.2 with AssetMapper; pin symfony/asset-mapper below 8.2 or wait for the follow-up. Vite and Encore paths are unaffected, so most users see zero impact.DOMContentLoaded mount never fires again. Mount on the swap event, and guard against double-mounting by checking for el.__vue_app__.createApp is its own world; a shared Pinia store needs an explicit setup step, otherwise two instances on one page silently diverge. The Symfony package sidesteps this by keeping components self-contained — do the same unless you actually need cross-island state.{{ }} escapes quotes — use {!! !!} on a pre-encoded payload, or better, a @json directive).How can I help you explore Laravel packages today?