tapansharma/breeze-svelte
Laravel Breeze preset for Svelte that scaffolds Inertia-powered authentication and app pages. Quickly get login, registration, password reset, email verification, and profile management running with a Svelte frontend.
This is a scaffold package, not a runtime dependency. It mirrors laravel/breeze but ships the Svelte + Tailwind variant of the auth stack: controllers, routes, migrations, Inertia page components, layouts and UI primitives. You run it once, it publishes files you then own.
v1.2.0 is a sync-hygiene release, not a feature release. The notes describe pruning the React and API stacks from the package's upstream-tracking tree (keeping Blade for UI diffs and Vue for JS diffs) and re-syncing. Nothing in the install path or published Svelte files is advertised as changed, so treat it as "same package, maintained again" rather than "new capabilities." Crucially, the notes don't say which upstream Breeze version it now tracks — check the pins yourself before assuming the Laravel 11 port is still required.
Install against a fresh app (do not install it next to laravel/breeze — both register the breeze:install command and will collide):
laravel new my-app && cd my-app
composer require tapansharma/breeze-svelte --dev
php artisan breeze:install # check the README — confirm the command name and any flags
npm install
php artisan migrate
npm run dev
Version check first. Open composer.json (laravel/framework) and package.json (svelte, vite, @sveltejs/vite-plugin-svelte, tailwindcss) in the package repo before you commit to a Laravel version. If those are still Laravel 10 / Svelte 4 / Vite 4, you're in the porting scenario below; if the re-sync pulled them forward, most of that work disappears.
Look here first, in this order:
| Path | Why |
|---|---|
routes/auth.php + routes/web.php |
The entire auth surface and how /dashboard is gated |
resources/js/app.js |
Inertia bootstrap; shows which adapter version and page resolution glob are in play |
resources/js/Pages/Auth/Login.svelte |
The canonical example of form → controller → redirect |
resources/js/Layouts/AuthenticatedLayout.svelte |
Where nav, user menu and flash messaging live |
vite.config.js / package.json |
Pin check — this is where breakage will come from |
resources/views/app.blade.php |
Root template: @vite, @inertia, @routes |
First use case: get register → verify → login → dashboard → logout working end-to-end, then change one thing (add a field to Profile/Edit.svelte, wire it through ProfileController@update) to learn the request/response loop. That single edit teaches you 90% of the day-to-day pattern.
On the Laravel 11+ question: still verify, don't assume. If the re-sync landed on a newer Breeze baseline, bootstrap/app.php middleware registration may already be correct. If it didn't, you'll be hand-porting middleware aliases and the missing Http/Kernel.php conventions — budget an afternoon. Either way, confirm by reading bootstrap/app.php rather than trusting the release notes, which only describe the sync process.
Adding a page (the loop you'll repeat constantly). Controller returns Inertia::render, a .svelte file appears in Pages/, and the glob in app.js picks it up — no manual registration:
Route::get('/reports', fn () => Inertia::render('Reports/Index', [
'reports' => Report::latest()->paginate(15),
]))->middleware(['auth', 'verified'])->name('reports.index');
<script>
export let reports
import AuthenticatedLayout from '../Layouts/AuthenticatedLayout.svelte'
</script>
<AuthenticatedLayout>
{#each reports.data as report (report.id)}
<p>{report.title}</p>
{/each}
</AuthenticatedLayout>
Pass props as arrays/toArray() — Eloquent models get serialized but paginators and relations behave predictably only when you're explicit.
Forms. Reuse the adapter's useForm so validation errors and processing come free from Laravel's ValidationException:
<script>
import { useForm } from '@inertiajs/svelte'
const form = useForm({ name: '', email: '' })
</script>
<input bind:value={form.name} />
{#if form.errors.name}<p class="text-red-600">{form.errors.name}</p>{/if}
<button on:click={() => form.put('/profile')} disabled={form.processing}>Save</button>
Never use raw fetch/axios for these flows — you lose the automatic error bag, the X-Inertia partial-reload headers, and the 303 redirect handling.
Shared data. Anything on every page (auth user, flash) goes through app/Http/Middleware/HandleInertiaRequests.php → share(). Use it for the current user rather than re-querying in each controller; AuthenticatedLayout already expects it.
Cross-component state. This is where Svelte earns its keep over the Vue/React Breeze stacks — reach for a plain Svelte store in resources/js/stores/ instead of prop-drilling through layouts:
// stores/toasts.js
import { writable } from 'svelte/store'
export const toasts = writable([])
Trigger it from an onSuccess callback in useForm or from a shared flash prop, and render one <Toasts /> in AuthenticatedLayout.
Navigation and URLs. Use Ziggy's route() helper in components (requires @routes in app.blade.php and tightenco/ziggy in composer.json). Use Inertia::render for full page loads and Inertia <Link> for anything internal so the SPA shell survives — plain <a href> triggers a full document reload and throws away your client state.
Authorization. Stick to Laravel's side: $this->authorize(), Gate, policies, and route middleware. Pass a computed can object down as a prop and drive {#if can.update} in Svelte rather than duplicating role logic in JS.
Finding reference implementations. The package tree no longer carries React or API stacks, so when you go looking for a parallel implementation of a feature, you'll only find Blade and Vue. That's fine — but it means React-based Breeze tutorials and GitHub snippets are now doubly useless to you (wrong stack and absent from the repo), while Vue diff-hunting is still viable for anything JS-side.
Once installed, consider composer remove tapansharma/breeze-svelte --dev. The reasoning has changed with v1.2.0 — the release proves it's still alive, so "abandoned" is no longer the argument. The structural argument stands: it has zero runtime dependents, everything it provides is now owned by your app, and keeping it in require-dev only risks a future composer update surprise.
v1.2.0 is not a version-bump announcement. The release notes describe what was removed from the sync tree, not what was upgraded. Do not read "everything else is synced properly" as "now on Laravel 12 / Svelte 5 / Vite 6." Treat the pins as unverified until you open package.json.
Toolchain drift is the main failure mode. Historically the pinned trio was roughly svelte@4, vite@4, @sveltejs/vite-plugin-svelte@2. Bumping Vite to 5/6 without bumping the plugin to 3/4 produces plugin API errors that look like syntax errors in your .svelte files. Whatever your pins are, upgrade them together, or not at all.
Tailwind version. This predates Tailwind v4. Running npm i tailwindcss@latest will break every utility in the shipped components (@tailwind base/components/utilities is gone, config moved to CSS). Pin tailwindcss@^3 unless you've confirmed the re-sync moved the config forward.
.blade.php and Vue leftovers. Because it's a fork of Breeze and explicitly keeps Blade and Vue stacks in-tree for diffing against upstream, you may find Blade auth views — and possibly stray .vue files — published alongside the Svelte ones. They're inert but confusing. Confirm routing goes through Inertia, then delete resources/views/auth/ and any Vue components you didn't wire up. Removing React/API in v1.2.0 reduced this surface; it didn't eliminate it.
npm run dev vs npm run build. Vite HMR plus Inertia is generally fine, but if you see a blank screen or a stale page after edits, it's almost always a manifest mismatch: run npm run build and check public/build/manifest.json exists. In APP_ENV=local the @vite directive wants the dev server running; if it's not, you get an unstyled page and 404s on /resources/js/app.js, not a PHP error.
Page-not-found errors. The import.meta.glob('./Pages/**/*.svelte') pattern is case-sensitive and path-sensitive. Inertia::render('reports/Index') will not resolve Pages/Reports/Index.svelte. Match the string to the filesystem exactly.
useForm reactivity. Depending on the installed @inertiajs/svelte version, useForm returns either a directly-reactive object or a store. If bindings don't update or you get $form is not defined, you're on the other convention — switch between form.errors.x and $form.errors.x to match. This is the single most common "why isn't this rendering" moment, and a re-sync is exactly the kind of event that can flip which convention you're on.
Laravel 11+ checklist (if the pins are still old). No app/Http/Kernel.php, middleware configured in bootstrap/app.php, and the default breeze:install assumptions about the skeleton are wrong. Port the auth middleware aliases (auth, guest, verified, password.confirm) into the new withMiddleware closure. Also verify VerifyEmail and ResetPassword routes — the verified middleware alias and MustVerifyEmail on User are both easy to miss, and the symptom is a silent redirect loop to /verify-email.
Debugging requests. Use the Laravel log plus HandleInertiaRequests temporarily dumping $request->header('X-Inertia'). A 409 in the network tab is an Inertia version mismatch — usually a stale npm run build after the package changed app.js. A 419 is the CSRF/XSRF cookie, fixed by ensuring @routes and @csrf-equivalent meta are present in the root blade.
Don't fight the scaffold. The shipped components (TextInput, PrimaryButton, Modal, Dropdown) are plain Tailwind and meant to be edited in place, not wrapped. Restyle them once and everything downstream — all six auth screens plus the profile page — follows.
Extension points worth using: the two layouts (swap nav globally), HandleInertiaRequests::share (global props), resources/js/Components/ (design system seed), and vite.config.js aliases ($lib, @ for resources/js) which save a lot of ../../.. in deep pages.
One structural warning: Svelte's {#if} blocks and slot syntax differ from the Vue/React examples you'll find in Breeze tutorials and Stack Overflow. Translate mentally — the PHP side is byte-for-byte identical, so Laravel-level answers always apply; only the markup doesn't.
How can I help you explore Laravel packages today?