Skip to main content

What is Svelte?

Svelte occupies a unique position among JavaScript frameworks. While React and Vue work as runtime libraries, Svelte works as a compiler. It converts components to pure JavaScript at build time, so no extra framework code needs to be shipped to the browser. Svelte’s biggest characteristic is that it doesn’t use a Virtual DOM. When state changes, code Svelte generated at compile time directly updates the DOM. This yields extremely lightweight and fast UIs.
This page describes the combination of Svelte 5 and Inertia v3. The Laravel 13 starter kit uses this stack by default.

Svelte 5 runes

Svelte 5 (released 2024) introduced a new reactivity system called Runes. You declare reactive state using special functions (runes) like $state, $derived, and $effect. Unlike the implicit reactivity of Svelte 4 and earlier, the design is explicit and predictable.
<script lang="ts">
    let count = $state(0)

    function increment() {
        count++
    }
</script>

<button onclick={increment}>{count}</button>
Laravel’s Svelte starter kit uses Svelte 5 + TypeScript as the standard. All examples on this page are written in TypeScript (lang="ts").

Svelte’s position in Laravel

History

Svelte’s relationship with Laravel is much newer than Vue’s or React’s; the first official support is starter kit adoption. With Laravel 13 in 2026, Svelte was officially added to the starter kits, making Svelte an official frontend option in the Laravel ecosystem. It’s treated as equal to Vue and React and is selectable in the laravel new interactive prompt. Svelte is still relatively unknown to many Laravel users, but its compiler-driven small bundles and simple syntax stand out—users switching from Vue or React are often surprised by how little they need to write.

The current mainstream style: Inertia × Svelte

The center of gravity for Svelte in Laravel today is Inertia × Svelte. Inertia enables a “modern monolith” architecture where data flows from Laravel controllers directly to Svelte components without designing an API.

Setup

For new projects, using a starter kit is the easiest way.
laravel new my-app
Choosing Svelte in the interactive prompt sets up all of the following automatically:
  • inertiajs/inertia-laravel (server-side adapter)
  • @inertiajs/svelte (client adapter)
  • svelte + @sveltejs/vite-plugin-svelte (Svelte 5 itself and the Vite plugin)
  • TypeScript + svelte-check
  • Tailwind CSS + the shadcn-svelte component library
  • The HandleInertiaRequests middleware
  • Auth screens like login and registration (already implemented in Inertia + Svelte + TypeScript)

Manual installation

To add it to an existing project, install the server-side and client-side pieces separately.
# Server side (PHP)
composer require inertiajs/inertia-laravel

# Client side (JavaScript)
npm install @inertiajs/svelte @inertiajs/vite svelte
npm install --save-dev @sveltejs/vite-plugin-svelte svelte-check typescript
Next, add the Svelte plugin and the Inertia Vite plugin to vite.config.ts.
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import inertia from '@inertiajs/vite'

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.ts'],
            refresh: true,
        }),
        svelte(),
        inertia(),
    ],
})
Start the Inertia app from resources/js/app.ts. The @inertiajs/vite plugin handles page auto-resolution and mounting, so only a minimal entry point is needed.
import { createInertiaApp } from '@inertiajs/svelte'

createInertiaApp()
For manual installation details (root template configuration, middleware registration, etc.), see the Inertia official docs.

Directory structure

In the starter kits, Svelte page components live in the resources/js/pages/ directory.
resources/js/
├── app.ts             # Inertia app entry point
├── components/        # Reusable UI components
│   └── ui/            # shadcn-svelte components
├── layouts/           # Layout components
│   ├── AppLayout.svelte
│   └── AuthLayout.svelte
├── lib/               # Utility functions and Svelte rune modules
├── pages/             # Inertia page components (mirror controller names)
│   ├── auth/
│   │   ├── Login.svelte
│   │   └── Register.svelte
│   ├── Dashboard.svelte
│   └── posts/
│       ├── Index.svelte
│       ├── Create.svelte
│       └── Show.svelte
└── types/             # TypeScript type definitions
Writing Inertia::render('posts/Index', [...]) maps to the component at resources/js/pages/posts/Index.svelte.

Basic structure of a Svelte file

A .svelte file consists of three blocks: <script>, template, and <style>.
<script lang="ts">
    // Logic (TypeScript)
    let name = $state('Laravel')
</script>

<!-- Template (HTML-like syntax) -->
<h1>Hello, {name}!</h1>

<style>
    /* Scoped CSS (applies only to this component) */
    h1 {
        color: #ff2d20;
    }
</style>
The structure resembles Vue’s Single File Components (SFCs), but there’s less markup and script to write. Because <style> is scoped by default, you don’t need to worry about class name collisions.

Template syntax

Variable interpolation and expressions

Inside the template, use {} to embed JavaScript values and expressions.
<script lang="ts">
    let name = $state('world')
    let count = $state(3)
</script>

<p>Hello, {name}!</p>
<p>Doubled: {count * 2}</p>

{#if} — conditionals

<script lang="ts">
    let isLoggedIn = $state(false)
    let role = $state('editor')
</script>

{#if isLoggedIn}
    <p>Welcome!</p>
{:else if role === 'admin'}
    <p>Logged in as admin</p>
{:else}
    <a href="/login">Log in</a>
{/if}
Equivalent to Vue’s v-if / v-else or React’s ternary operator.

{#each} — list rendering

<script lang="ts">
    type Post = { id: number; title: string }
    let posts = $state<Post[]>([
        { id: 1, title: 'First post' },
        { id: 2, title: 'Second post' },
    ])
</script>

<ul>
    {#each posts as post (post.id)}
        <li>{post.title}</li>
    {/each}
</ul>
(post.id) specifies the key, used for efficient diffing. Equivalent to Vue’s v-for or React’s Array.map().

bind: — two-way binding

Use bind:value to two-way sync a form element’s value with a reactive variable.
<script lang="ts">
    let title = $state('')
    let agreed = $state(false)
    let role = $state('viewer')
</script>

<!-- Text input -->
<input bind:value={title} type="text" />
<p>Typing: {title}</p>

<!-- Checkbox -->
<input bind:checked={agreed} type="checkbox" />
<p>Agreed: {agreed}</p>

<!-- Select box -->
<select bind:value={role}>
    <option value="viewer">Viewer</option>
    <option value="editor">Editor</option>
    <option value="admin">Admin</option>
</select>
Equivalent to Vue’s v-model. React requires you to hand-wire onChange handlers, but Svelte lets you write this declaratively with bind:.

Page component basics

Inertia page components are ordinary Svelte components. Data passed from a Laravel controller arrives as props.

Controller

// app/Http/Controllers/PostController.php
use Inertia\Inertia;
use App\Models\Post;

class PostController extends Controller
{
    public function index()
    {
        return Inertia::render('posts/Index', [
            'posts' => Post::latest()->paginate(10),
        ]);
    }
}

Svelte page component

In Svelte 5, use the $props() rune to receive props.
<!-- resources/js/pages/posts/Index.svelte -->
<script lang="ts">
    import { Link } from '@inertiajs/svelte'

    type Post = {
        id: number
        title: string
        created_at: string
    }

    type Props = {
        posts: {
            data: Post[]
        }
    }

    let { posts }: Props = $props()
</script>

<div>
    <h1>Posts</h1>
    {#each posts.data as post (post.id)}
        <article>
            <h2>
                <Link href={`/posts/${post.id}`}>{post.title}</Link>
            </h2>
            <p>{post.created_at}</p>
        </article>
    {/each}
</div>
Just receive props with $props() and use them in the template. No REST API definition needed.
The <Link> component from @inertiajs/svelte routes page transitions via XHR, avoiding a full browser reload.
<script lang="ts">
    import { Link } from '@inertiajs/svelte'
</script>

<!-- Basic link -->
<Link href="/posts">Posts</Link>

<!-- Link with POST method (e.g. delete) -->
<Link href="/posts/1" method="delete" as="button">
    Delete
</Link>

<!-- Preload (fetch ahead of time on hover) -->
<Link href="/posts/1" preload>View post</Link>
You write it like a normal <a> tag, but Inertia swaps only the page component behind the scenes, giving you an SPA-like feel.

The Form component

The <Form> component from @inertiajs/svelte is the recommended form submission style used in the starter kit auth screens. Specify action and method as props and write the interior with a {#snippet}.

Basic usage

<script lang="ts">
    import { Form } from '@inertiajs/svelte'
</script>

<Form action="/posts" method="post" class="flex flex-col gap-4">
    {#snippet children({ errors, processing })}
        <div>
            <label for="title">Title</label>
            <input id="title" name="title" type="text" required />
            {#if errors.title}
                <p class="error">{errors.title}</p>
            {/if}
        </div>

        <div>
            <label for="content">Content</label>
            <textarea id="content" name="content"></textarea>
            {#if errors.content}
                <p class="error">{errors.content}</p>
            {/if}
        </div>

        <button type="submit" disabled={processing}>
            {processing ? 'Submitting...' : 'Post'}
        </button>
    {/snippet}
</Form>
{#snippet children({ errors, processing })} is Svelte’s snippet syntax—a content block passed to a component (equivalent to Vue slots or React render props). errors and processing are computed and provided automatically by the Form component. Form fields use HTML-native name attributes rather than bind:value, and the browser’s standard form data collection does the work.

Starter kit pattern

The starter kit uses Wayfinder to manage routes as objects. store.form() returns an object containing the route object’s action and method, which is spread onto <Form>.
<script lang="ts">
    import { Form } from '@inertiajs/svelte'
    import { store } from '@/routes/login'
</script>

<Form
    {...store.form()}
    resetOnSuccess={['password']}
    class="flex flex-col gap-6"
>
    {#snippet children({ errors, processing })}
        <!-- Form contents -->
    {/snippet}
</Form>
Fields listed in resetOnSuccess are automatically reset on successful submission. Use it for fields like password fields that should be cleared after submit.
Without Wayfinder, pass the URL directly like action="/login" and it behaves the same.

The useForm hook

For form handling, use @inertiajs/svelte’s useForm hook. It cleanly implements form state management, submission, and validation error display.

Controller side

// app/Http/Controllers/PostController.php
class PostController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title'   => ['required', 'string', 'max:255'],
            'content' => ['required', 'string'],
        ]);

        Post::create($validated + ['user_id' => auth()->id()]);

        return redirect()->route('posts.index')
            ->with('success', 'Post created.');
    }
}

Svelte form component

<!-- resources/js/pages/posts/Create.svelte -->
<script lang="ts">
    import { useForm } from '@inertiajs/svelte'

    const form = useForm({
        title: '',
        content: '',
    })

    function submit(e: SubmitEvent) {
        e.preventDefault()
        form.post('/posts')
    }
</script>

<form onsubmit={submit}>
    <div>
        <label>Title</label>
        <input type="text" bind:value={form.data.title} />
        {#if form.errors.title}
            <p class="error">{form.errors.title}</p>
        {/if}
    </div>

    <div>
        <label>Content</label>
        <textarea bind:value={form.data.content}></textarea>
        {#if form.errors.content}
            <p class="error">{form.errors.content}</p>
        {/if}
    </div>

    <button type="submit" disabled={form.processing}>
        {form.processing ? 'Submitting...' : 'Post'}
    </button>
</form>
Here are the main properties on the object returned by useForm.
Property / methodDescription
form.dataThe form’s data object
form.errorsValidation errors (access by field name)
form.processingtrue while submitting (used to disable the button)
form.isDirtytrue if changed from initial values
form.post(url)Submit via POST
form.put(url)Submit via PUT (for updates)
form.delete(url)Submit via DELETE
form.reset()Reset the form to initial values
When validation errors are returned, useForm displays them while preserving the input values. Combined with bind:value, it delivers a seamless form experience.

Shared data

Data needed on every page (the logged-in user, flash messages, etc.) is defined in the share() method of the HandleInertiaRequests middleware.
// app/Http/Middleware/HandleInertiaRequests.php
use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'auth' => [
                'user' => $request->user()
                    ? $request->user()->only('id', 'name', 'email')
                    : null,
            ],
            'flash' => [
                'success' => $request->session()->get('success'),
                'error'   => $request->session()->get('error'),
            ],
        ]);
    }
}
Use usePage() to access shared data from a Svelte component.
<script lang="ts">
    import { usePage } from '@inertiajs/svelte'

    type SharedProps = {
        auth: {
            user: { id: number; name: string; email: string } | null
        }
        flash: {
            success: string | null
            error: string | null
        }
    }

    const page = usePage<SharedProps>()
</script>

<header>
    {#if page.props.auth.user}
        <span>{page.props.auth.user.name}</span>
    {:else}
        <span>Guest</span>
    {/if}
</header>

{#if page.props.flash.success}
    <div class="alert-success">{page.props.flash.success}</div>
{/if}
Because shared data is included on every request, keep it to the minimum needed. Wrapping in fn() for lazy evaluation ensures it’s only evaluated when actually accessed.

Svelte 5 reactivity (runes)

Here are the Svelte 5 runes to know when developing with Inertia × Svelte.

$state — reactive state

<script lang="ts">
    let count = $state(0)
    let isOpen = $state(false)
    let items = $state<string[]>([])
</script>

<p>{count}</p>
<button onclick={() => count++}>+1</button>
<button onclick={() => isOpen = !isOpen}>Toggle</button>
Variables declared with $state become automatically reactive. When the value changes, the DOM updates automatically. It corresponds to Vue’s ref or React’s useState, but you don’t need .value—just assign to update state.

$derived — derived values

<script lang="ts">
    let posts = $state<{ title: string; published: boolean }[]>([])

    // Extract only published posts
    let publishedPosts = $derived(posts.filter(post => post.published))

    // With multiple dependencies
    let summary = $derived.by(() => {
        const total = posts.length
        const published = publishedPosts.length
        return `${published} of ${total} posts published`
    })
</script>

<p>{summary}</p>
$derived automatically recomputes when its dependencies change. Corresponds to Vue’s computed or React’s useMemo.

$effect — side effects

<script lang="ts">
    let query = $state('')

    // Runs whenever query changes
    $effect(() => {
        console.log('Query changed:', query)

        // You can return a cleanup function
        return () => {
            console.log('Cleanup')
        }
    })
</script>

<input bind:value={query} placeholder="Search..." />
$effect runs each time a $state dependency changes. It corresponds to React’s useEffect, but you don’t need to specify a dependency array—the $state variables used are tracked automatically.

shadcn-svelte components

The starter kit includes shadcn-svelte. shadcn-svelte is a component library with the same design philosophy as shadcn/ui for React—copy the code into your project and freely customize.

Adding a component

npx shadcn-svelte@latest add button
npx shadcn-svelte@latest add input
npx shadcn-svelte@latest add card
Running the command places the component source into resources/js/components/ui/.

Usage

<script lang="ts">
    import { Button } from '@/components/ui/button'
    import { Input } from '@/components/ui/input'
    import * as Card from '@/components/ui/card'
    import { useForm } from '@inertiajs/svelte'

    const form = useForm({ email: '', password: '' })

    function submit(e: SubmitEvent) {
        e.preventDefault()
        form.post('/login')
    }
</script>

<Card.Root class="w-96">
    <Card.Header>
        <Card.Title>Log in</Card.Title>
    </Card.Header>
    <Card.Content>
        <form onsubmit={submit} class="space-y-4">
            <Input
                type="email"
                bind:value={form.data.email}
                placeholder="Email"
            />
            {#if form.errors.email}
                <p class="text-sm text-red-500">{form.errors.email}</p>
            {/if}

            <Input
                type="password"
                bind:value={form.data.password}
                placeholder="Password"
            />

            <Button type="submit" disabled={form.processing} class="w-full">
                {form.processing ? 'Signing in...' : 'Sign in'}
            </Button>
        </form>
    </Card.Content>
</Card.Root>
The starter kit already includes commonly used components like Button, Input, Card, Dialog, and Dropdown. Add more as needed with npx shadcn-svelte@latest add.

Summary

Svelte shines when paired with Laravel, especially in a “modern monolith” configuration through Inertia. The compiler-based design keeps bundle sizes small, and the runes syntax makes reactivity explicit and easy to understand.
ElementRole
Laravel controllerRouting, data retrieval, validation
Inertia::render()Pass data from controller to Svelte component
Svelte page componentReceive props with $props() and render UI
useFormForm state management, submission, error display
Link componentPage transitions without a full reload
usePage().propsAccess shared data
shadcn-svelteStandard component library
Inertia × Svelte combines the simplicity of a Laravel backend with Svelte’s compact style, offering a great development experience. Creating a project from the starter kit gets you started immediately, auth screens included.

Inertia.js official docs

See the official documentation for all Inertia v3 features.
Last modified on July 13, 2026