Zum Hauptinhalt springen

Was ist Svelte?

Svelte nimmt unter den JavaScript-Frameworks eine besondere Position ein. Während React und Vue als Laufzeitbibliotheken arbeiten, wirkt Svelte als Compiler. Beim Build werden Komponenten in reines JavaScript umgewandelt, sodass keine zusätzliche Framework-Runtime an den Browser gesendet werden muss. Das prägnanteste Merkmal von Svelte ist der Verzicht auf einen Virtual DOM. Bei Zustandsänderungen aktualisiert der von Svelte zur Compile-Zeit erzeugte Code das DOM direkt. Dadurch entstehen sehr leichte, schnelle Oberflächen.
Diese Seite behandelt die Kombination Svelte 5 mit Inertia v3. Die Starterkits von Laravel 13 verwenden diese Konstellation standardmäßig.

Runen in Svelte 5

Mit Svelte 5 (Release 2024) wurde ein neues Reaktivitätssystem eingeführt: die Runen (Runes). Reaktiver State wird mit besonderen Funktionen (Runen) wie $state, $derived und $effect deklariert. Anders als die implizite Reaktivität in Svelte 4 ist das Design explizit und leichter vorhersagbar.
<script lang="ts">
    let count = $state(0)

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

<button onclick={increment}>{count}</button>
Das Svelte-Starterkit von Laravel setzt standardmäßig auf Svelte 5 mit TypeScript. Alle Beispiele auf dieser Seite sind ebenfalls in TypeScript (lang="ts") geschrieben.

Rolle in Laravel

Historie

Die Beziehung zwischen Svelte und Laravel ist gegenüber Vue und React sehr jung – der erste offizielle Support ist die Aufnahme in das offizielle Starterkit. Mit Laravel 13 (2026) wurde Svelte offiziell den Starterkits hinzugefügt und ist damit eine offizielle Frontend-Option im Laravel-Ökosystem. Es steht gleichrangig neben Vue und React und lässt sich im interaktiven Prompt von laravel new auswählen. Für viele Laravel-Nutzer ist Svelte noch weniger bekannt; kennzeichnend sind der kleine Bundle-Footprint durch den Compiler und die schlanke Syntax. Wer von Vue oder React umsteigt, ist häufig überrascht, wie wenig Code nötig ist.

Aktueller Standard: Inertia × Svelte

Der Standardansatz für Svelte in Laravel ist Inertia × Svelte. Inertia realisiert die „moderne Monolith”-Architektur, bei der Daten aus dem Laravel-Controller direkt an Svelte-Komponenten gehen, ohne API-Design.

Setup

Via Starterkit (empfohlen)

Für neue Projekte ist das Starterkit am komfortabelsten.
laravel new my-app
Wenn Sie im interaktiven Prompt Svelte wählen, wird automatisch alles Folgende eingerichtet:
  • inertiajs/inertia-laravel (serverseitiger Adapter)
  • @inertiajs/svelte (Client-Adapter)
  • svelte + @sveltejs/vite-plugin-svelte (Svelte 5 selbst und Vite-Plugin)
  • TypeScript + svelte-check
  • Tailwind CSS + Komponentenbibliothek shadcn-svelte
  • Middleware HandleInertiaRequests
  • Auth-Seiten für Login, Registrierung u. a. (bereits mit Inertia + Svelte + TypeScript umgesetzt)

Manuelle Installation

Für die Ergänzung in einem bestehenden Projekt installieren Sie Server- und Client-Seite separat.
# Serverseitig (PHP)
composer require inertiajs/inertia-laravel

# Clientseitig (JavaScript)
npm install @inertiajs/svelte @inertiajs/vite svelte
npm install --save-dev @sveltejs/vite-plugin-svelte svelte-check typescript
Ergänzen Sie anschließend in vite.config.ts das Svelte-Plugin und das Inertia-Vite-Plugin:
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(),
    ],
})
Starten Sie die Inertia-App in resources/js/app.ts. Da das Plugin @inertiajs/vite die automatische Auflösung und das Mounting übernimmt, genügt ein minimaler Entry-Point.
import { createInertiaApp } from '@inertiajs/svelte'

createInertiaApp()
Details zur manuellen Installation (Root-Template, Middleware-Registrierung usw.) finden Sie in der offiziellen Inertia-Dokumentation.

Verzeichnisstruktur

Im Starterkit werden Svelte-Page-Komponenten unter resources/js/pages/ abgelegt.
resources/js/
├── app.ts             # Einstiegspunkt der Inertia-App
├── components/        # Wiederverwendbare UI-Komponenten
│   └── ui/            # shadcn-svelte-Komponenten
├── layouts/           # Layout-Komponenten
│   ├── AppLayout.svelte
│   └── AuthLayout.svelte
├── lib/               # Utility-Funktionen und Svelte-Runen-Module
├── pages/             # Inertia-Page-Komponenten (analog zu Controller-Namen)
│   ├── auth/
│   │   ├── Login.svelte
│   │   └── Register.svelte
│   ├── Dashboard.svelte
│   └── posts/
│       ├── Index.svelte
│       ├── Create.svelte
│       └── Show.svelte
└── types/             # TypeScript-Typdefinitionen
Inertia::render('posts/Index', [...]) verweist auf resources/js/pages/posts/Index.svelte.

Grundaufbau einer Svelte-Datei

Eine .svelte-Datei besteht aus drei Blöcken: <script>, Template und <style>.
<script lang="ts">
    // Logik (TypeScript)
    let name = $state('Laravel')
</script>

<!-- Template (HTML-ähnliche Notation) -->
<h1>Hello, {name}!</h1>

<style>
    /* Scoped CSS (nur auf diese Komponente angewendet) */
    h1 {
        color: #ff2d20;
    }
</style>
Ähnlich wie die Single-File-Components (SFC) von Vue, allerdings mit weniger Boilerplate in Template und Script. <style> ist standardmäßig gescoped, sodass Sie sich um Klassennamen-Kollisionen nicht sorgen müssen.

Template-Syntax

Variablen und Ausdrücke

Innerhalb des Templates betten Sie JavaScript-Werte mit {} ein.
<script lang="ts">
    let name = $state('Welt')
    let count = $state(3)
</script>

<p>Hallo, {name}!</p>
<p>Doppelter Wert: {count * 2}</p>

{#if} – Bedingte Anzeige

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

{#if isLoggedIn}
    <p>Willkommen!</p>
{:else if role === 'admin'}
    <p>Als Administrator angemeldet</p>
{:else}
    <a href="/login">Anmelden</a>
{/if}
Entspricht v-if / v-else in Vue und dem ternären Operator in React.

{#each} – Listen darstellen

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

<ul>
    {#each posts as post (post.id)}
        <li>{post.title}</li>
    {/each}
</ul>
(post.id) ist die Key-Angabe und wird für effizientes Diffing genutzt. Entspricht v-for in Vue und Array.map() in React.

bind: – Zwei-Wege-Binding

Mit bind:value synchronisieren Sie den Wert eines Formularelements bidirektional mit einer reaktiven Variablen.
<script lang="ts">
    let title = $state('')
    let agreed = $state(false)
    let role = $state('viewer')
</script>

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

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

<!-- Select -->
<select bind:value={role}>
    <option value="viewer">Betrachter</option>
    <option value="editor">Bearbeiter</option>
    <option value="admin">Administrator</option>
</select>
Entspricht v-model in Vue. Anders als bei React, wo onChange-Handler nötig sind, drücken Sie es in Svelte deklarativ mit bind: aus.

Grundlagen der Page-Komponente

Eine Inertia-Page-Komponente ist eine gewöhnliche Svelte-Komponente. Die aus dem Laravel-Controller übergebenen Daten stehen als Props zur Verfügung.

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-Komponente

In Svelte 5 empfangen Sie Props mit der Rune $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>Beitragsübersicht</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>
Nach $props() verwenden Sie die vom Controller übergebenen Daten direkt im Template. Eine REST-API ist nicht nötig.
Die von @inertiajs/svelte bereitgestellte <Link>-Komponente sorgt für Seitenwechsel per XHR und vermeidet ein Full-Page-Reload.
<script lang="ts">
    import { Link } from '@inertiajs/svelte'
</script>

<!-- Einfacher Link -->
<Link href="/posts">Beitragsübersicht</Link>

<!-- Link per POST (z. B. Löschen) -->
<Link href="/posts/1" method="delete" as="button">
    Löschen
</Link>

<!-- Preload (bei Hover vorab laden) -->
<Link href="/posts/1" preload>Beitrag ansehen</Link>
Die Schreibweise gleicht einem <a>-Tag; im Hintergrund tauscht Inertia nur die Page-Komponente aus.

Die Form-Komponente

Die <Form>-Komponente von @inertiajs/svelte ist der empfohlene Stil aus den Starterkit-Auth-Screens. Sie geben action und method als Props an und beschreiben das Innere mit {#snippet}.

Grundlegende Verwendung

<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">Titel</label>
            <input id="title" name="title" type="text" required />
            {#if errors.title}
                <p class="error">{errors.title}</p>
            {/if}
        </div>

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

        <button type="submit" disabled={processing}>
            {processing ? 'Wird gesendet...' : 'Veröffentlichen'}
        </button>
    {/snippet}
</Form>
{#snippet children({ errors, processing })} ist die Snippet-Syntax von Svelte – ein Content-Block, der an die Komponente übergeben wird (analog zu Vue-Slots oder React-Render-Props). errors und processing werden von Form automatisch berechnet und übergeben. Statt bind:value verwenden Sie das native HTML-name-Attribut, sodass die Formulardatenerfassung des Browsers zum Zug kommt.

Muster aus dem Starterkit

Die Starterkits verwalten Routen mit Wayfinder als Objekte. store.form() liefert ein Objekt mit action und method, das per Spread an <Form> übergeben wird.
<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 })}
        <!-- Formularinhalt -->
    {/snippet}
</Form>
Die in resetOnSuccess angegebenen Felder werden bei erfolgreichem Absenden automatisch zurückgesetzt. Nutzen Sie das etwa für Passwortfelder, die nach dem Absenden geleert werden sollen.
Ohne Wayfinder verhalten sich action="/login" und Co. identisch, sobald Sie die URL direkt übergeben.

Der useForm-Hook

Für die Formularverarbeitung nutzen Sie den useForm-Hook aus @inertiajs/svelte. State-Handling, Absenden und Fehleranzeige lassen sich damit schlank umsetzen.

Auf der Controller-Seite

// 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', 'Der Beitrag wurde erstellt.');
    }
}

Svelte-Formular-Komponente

<!-- 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>Titel</label>
        <input type="text" bind:value={form.data.title} />
        {#if form.errors.title}
            <p class="error">{form.errors.title}</p>
        {/if}
    </div>

    <div>
        <label>Inhalt</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 ? 'Wird gesendet...' : 'Veröffentlichen'}
    </button>
</form>
Wichtige Eigenschaften des Rückgabewerts von useForm:
Eigenschaft / MethodeBeschreibung
form.dataDatenobjekt des Formulars
form.errorsValidierungsfehler (Zugriff per Feldname)
form.processingtrue, solange gesendet wird (für Button-Deaktivierung)
form.isDirtytrue, wenn sich der Wert vom Ursprung unterscheidet
form.post(url)Per POST senden
form.put(url)Per PUT senden (Update)
form.delete(url)Per DELETE senden
form.reset()Formular auf Initialwerte zurücksetzen
Bei Validierungsfehlern behält useForm die Eingaben und zeigt die Fehler an. In Verbindung mit bind:value entsteht ein nahtloses Formularerlebnis.

Shared Data

Daten, die auf allen Seiten gemeinsam gebraucht werden (angemeldeter Nutzer, Flash-Messages u. a.), definieren Sie in der Methode share() der Middleware HandleInertiaRequests.
// 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'),
            ],
        ]);
    }
}
In der Svelte-Komponente greifen Sie mit usePage() auf Shared Data zu.
<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>Gast</span>
    {/if}
</header>

{#if page.props.flash.success}
    <div class="alert-success">{page.props.flash.success}</div>
{/if}
Da Shared Data in jeder Anfrage enthalten sind, empfiehlt sich die Beschränkung auf das Nötigste. Eine Lazy-Evaluation per fn() wird nur ausgewertet, wenn tatsächlich zugegriffen wird.

Reaktivität in Svelte 5 (Runen)

Wichtige Runen-Konzepte für die Arbeit mit Inertia × Svelte:

$state – reaktiver 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>
Mit $state deklarierte Variablen sind automatisch reaktiv. Bei Wertänderung wird das DOM aktualisiert. Entspricht Vues ref bzw. Reacts useState, jedoch ohne .value-Zugriff – die Zuweisung reicht.

$derived – abgeleitete Werte

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

    // Nur Beiträge mit published = true
    let publishedPosts = $derived(posts.filter(post => post.published))

    // Mehrere Abhängigkeiten
    let summary = $derived.by(() => {
        const total = posts.length
        const published = publishedPosts.length
        return `${published} von ${total} veröffentlicht`
    })
</script>

<p>{summary}</p>
$derived berechnet sich neu, sobald sich abhängige Werte ändern. Entspricht computed in Vue und useMemo in React.

$effect – Seiteneffekte

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

    // Wird bei jeder Änderung von query ausgeführt
    $effect(() => {
        console.log('Suchanfrage geändert:', query)

        // Optional: Cleanup-Funktion zurückgeben
        return () => {
            console.log('Cleanup')
        }
    })
</script>

<input bind:value={query} placeholder="Suchen..." />
$effect wird bei jeder Änderung eines abhängigen $state-Werts ausgeführt. Entspricht useEffect in React, aber ohne explizites Dependency-Array – verwendete $state-Variablen werden automatisch getrackt.

shadcn-svelte-Komponenten

Das Starterkit enthält shadcn-svelte. shadcn-svelte folgt dem gleichen Designprinzip wie shadcn/ui für React: Sie kopieren den Code in Ihr Projekt und passen ihn frei an.

Komponente hinzufügen

npx shadcn-svelte@latest add button
npx shadcn-svelte@latest add input
npx shadcn-svelte@latest add card
Nach dem Befehl liegt der Quellcode der Komponenten in resources/js/components/ui/.

Verwendung

<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>Anmelden</Card.Title>
    </Card.Header>
    <Card.Content>
        <form onsubmit={submit} class="space-y-4">
            <Input
                type="email"
                bind:value={form.data.email}
                placeholder="E-Mail-Adresse"
            />
            {#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="Passwort"
            />

            <Button type="submit" disabled={form.processing} class="w-full">
                {form.processing ? 'Anmeldung...' : 'Anmelden'}
            </Button>
        </form>
    </Card.Content>
</Card.Root>
Im Starterkit sind bereits gängige Komponenten wie Button, Input, Card, Dialog und Dropdown enthalten. Weitere Komponenten ergänzen Sie bei Bedarf mit npx shadcn-svelte@latest add.

Fazit

Svelte entfaltet in Kombination mit Laravel – insbesondere über Inertia im Sinne eines „modernen Monolithen” – seine Stärken. Dank der Compiler-Basis ist die Bundle-Größe klein; die Runen-Syntax macht Reaktivität explizit und verständlich.
BausteinRolle
Laravel-ControllerRouting, Datenerfassung, Validierung
Inertia::render()Daten vom Controller an die Svelte-Komponente reichen
Svelte-Page-KomponenteProps per $props() empfangen und UI rendern
useFormFormular-State, Absenden und Fehleranzeige
Link-KomponenteSeitenwechsel ohne Full-Reload
usePage().propsZugriff auf Shared Data
shadcn-svelteStandard-Komponentenbibliothek
Mit Inertia × Svelte verbinden Sie die Einfachheit des Laravel-Backends mit der kompakten Ausdrucksweise von Svelte. Legen Sie ein Projekt mit dem Starterkit an, sind auch die Auth-Screens sofort einsatzbereit.

Offizielle Inertia.js-Dokumentation

Alle Funktionen von Inertia v3 finden Sie in der offiziellen Dokumentation.
Zuletzt geändert am 13. Juli 2026