# Slots, Context und Web Components in Svelte

URL: https://www.mikebild.dev/de/blog/svelte-slots-context-und-web-components/

Eine wiederverwendbare Card, in die jedes Team seinen eigenen Header und seinen eigenen Inhalt stecken kann: Die Anforderung klingt harmlos, und mit Props allein bekommt man sie trotzdem nicht sauber gelöst. Im Review tauchen dann die Ausweichmanöver auf – ein Store, wo ein Slot gereicht hätte; ein Prop, das durch sieben Komponenten gefädelt wird, obwohl die Context-API der kürzere Weg gewesen wäre; eine Komponentenbibliothek, die nur unter Svelte läuft, obwohl framework-neutrale Wiederverwendung das eigentliche Ziel war.

Slots, die Context-API und Custom Elements tauchen in denselben Komponenten nebeneinander auf und werden deshalb gern verwechselt. Auseinander hält sie die Richtung, in die etwas fließt: Markup von außen in eine Komponente hinein, Daten von innen an dieses Markup zurück, Werte nach unten durch beliebig viele Ebenen des Komponentenbaums. Wer vor der Werkzeugwahl kurz klärt, welche dieser Richtungen er gerade braucht, hat die Entscheidung meist schon getroffen.

**Redaktionelle Einordnung (Juli 2026):** Der Titel meint ausdrücklich Svelte 3. In Svelte 5 sind Snippets und `{@render ...}` die bevorzugte Komposition anstelle von Slots; Callback-Props ersetzen für neuen Code viele Component Events. Context und Custom Elements bestehen weiter. Die historische Syntax bleibt hier stehen, die Quellen zeigen zusätzlich die heutigen Gegenstücke.

## Slots: Inhalt von außen hineinreichen

Ein Slot ist ein Platzhalter in einer Komponente, den der Aufrufer von außen füllt. Die Card-Komponente entscheidet über Rahmen, Abstände und Layout – was innen steht, bestimmt der Aufrufer. Der einfachste Fall ist der Default-Slot:

```svelte
<!-- Card.svelte -->
<div class="card">
  <slot>Nothing here yet</slot>
</div>

<style>
  .card {
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 1rem;
  }
</style>
```

Der Inhalt zwischen den `<slot>`-Tags ist der Fallback: Er erscheint nur, wenn der Aufrufer nichts übergibt. Genutzt wird die Card dann so:

```svelte
<Card>
  <p>This paragraph lands inside the card.</p>
</Card>
```

Interessant wird es mit benannten Slots. Eine Card hat oft mehr als eine Zone – einen Header und einen Body etwa. In der Kind-Komponente vergibt man dafür Namen, im Aufruf ordnet man Markup diesen Namen zu:

```svelte
<!-- Card.svelte -->
<div class="card">
  {#if $$slots.header}
    <header class="card-header">
      <slot name="header"></slot>
    </header>
  {/if}
  <div class="card-body">
    <slot></slot>
  </div>
</div>
```

```svelte
<Card>
  <h2 slot="header">Release notes</h2>
  <p>Everything else fills the default slot.</p>
</Card>
```

Das Objekt `$$slots` verdient einen kurzen Blick. Seine Schlüssel sind die tatsächlich übergebenen Slot-Namen, und damit lässt sich die Struktur bedingt rendern. Der `<header>`-Rahmen erscheint nur, wenn der Aufrufer wirklich einen Header übergeben hat – sonst hätte man einen leeren, umrandeten Kasten. Das ist der Unterschied zwischen einer Komponente, die man gerne benutzt, und einer, die man umständlich findet.

Wichtig ist dabei, dass `$$slots` nur die Anwesenheit eines Slots meldet, nicht dessen Inhalt zur Laufzeit. Ob ein benannter Slot befüllt ist, entscheidet sich beim Kompilieren anhand des Aufrufs, nicht durch das, was der Aufrufer später hineinrendert. Für die typische Aufgabe – Rahmen weglassen, wenn niemand einen Header liefert – reicht das genau aus, und man sollte `$$slots` nicht mit einer reaktiven Prüfung auf leeren Inhalt verwechseln. Ein zweiter Punkt, der in Reviews regelmäßig auffällt: Ein benannter Slot lässt sich nur an einer Stelle einer Komponente ausgeben. Wer denselben benannten Slot zweimal rendert, bekommt keinen doppelten Inhalt, sondern ein Verhalten, auf das man sich nicht verlassen sollte. Braucht man denselben übergebenen Inhalt an mehreren Stellen, führt der Weg über slot props oder über eine Umstrukturierung der Komponente, nicht über ein zweites `<slot name="…">`.

### Slot props: Daten von innen nach außen

Bis hierher fließt Inhalt nur in eine Richtung: von außen nach innen. Der zweite Teil dreht diese Richtung um, und genau das wird am häufigsten verwechselt. Slot props erlauben es der Kind-Komponente, Daten nach außen an das eingesetzte Markup zu geben. Der klassische Fall ist eine Liste, die das Iterieren übernimmt, aber die Darstellung jedes Eintrags dem Aufrufer überlässt:

```svelte
<!-- List.svelte -->
<script>
  export let items = [];
</script>

<ul>
  {#each items as item, index}
    <li>
      <slot name="item" {item} {index}>
        {item}
      </slot>
    </li>
  {/each}
</ul>
```

Die Kind-Komponente gibt `item` und `index` über den Slot nach außen. Der Aufrufer empfängt diese Werte mit der `let:`-Directive und darf frei entscheiden, wie ein Eintrag aussieht:

```svelte
<List items={users}>
  <span slot="item" let:item let:index>
    {index + 1}. {item.name} – {item.role}
  </span>
</List>
```

Die Liste kümmert sich um Schleife, Schlüssel und Struktur; der Aufrufer bringt die konkrete Darstellung mit und bekommt dafür genau die Daten, die er braucht. Diese beiden Richtungen sauber auseinanderzuhalten ist der Kern des Slot-Verständnisses: Markup fließt von außen hinein, Daten fließen über slot props von innen nach außen. Das folgende Diagramm hält beide Wege nebeneinander fest.

```mermaid
flowchart LR
  Parent["Parent<br/>&lt;List let:item&gt;"]
  Child["List.svelte<br/>&lt;slot name='item' {item}&gt;"]
  Render["Rendered &lt;li&gt;"]

  Parent -- "markup (outside in)" --> Child
  Child -- "slot props: item, index (inside out)" --> Parent
  Child --> Render
```

## Context: durch den Baum reichen ohne Prop-Drilling

Slots lösen die Frage, wie man Inhalt an eine direkte Kind-Komponente gibt. Sie helfen nicht, wenn ein Wert mehrere Ebenen tief benötigt wird. Stellen wir uns eine Toolbar vor, deren einzelne Buttons sich bei ihr registrieren wollen. Zwischen Toolbar und Button liegen vielleicht noch Gruppen, Trenner, Wrapper. Den Registrierungs-Callback durch jede dieser Ebenen als Prop zu reichen, ist mühsam und macht Zwischenkomponenten von Details abhängig, die sie gar nichts angehen. Dieses Weiterreichen nennt man Prop-Drilling, und die Context-API ist die direkte Antwort darauf.

Die Provider-Komponente legt einen Wert unter einem Schlüssel ab, jede Nachfahr-Komponente kann ihn unter demselben Schlüssel wieder abrufen:

```svelte
<!-- Toolbar.svelte -->
<script>
  import { setContext } from 'svelte';

  const buttons = [];
  function registerButton(id) {
    buttons.push(id);
  }

  setContext('toolbar', { registerButton });
</script>

<div class="toolbar">
  <slot></slot>
</div>
```

```svelte
<!-- ToolbarButton.svelte -->
<script>
  import { getContext } from 'svelte';

  export let id;
  const toolbar = getContext('toolbar');
  toolbar.registerButton(id);
</script>

<button on:click>
  <slot></slot>
</button>
```

Der Button findet seine Toolbar, egal wie viele Ebenen dazwischenliegen. Kein Prop wandert durch die Zwischenkomponenten. Der Schlüssel darf übrigens ein beliebiger Wert sein – ein String wie hier, ein Symbol oder ein Objekt; ein Symbol schützt zuverlässig vor Namenskollisionen in größeren Repositories.

Der Geltungsbereich von Context ist dabei streng auf den Teilbaum unterhalb des Providers beschränkt. Eine Nachbarkomponente, die nicht unter derselben `setContext`-Komponente hängt, sieht den Wert nicht – Context fließt ausschließlich nach unten, nie zur Seite und nie nach oben. Ruft eine Kind-Komponente ihrerseits `setContext` mit demselben Schlüssel auf, überschattet sie den Wert für ihren eigenen Teilbaum, ohne den Wert weiter oben zu verändern; die Auflösung folgt also der Instanzhierarchie, nicht der Reihenfolge im Quelltext. Wer sich nicht sicher ist, ob ein Schlüssel überhaupt gesetzt wurde, kann das mit `hasContext` prüfen, statt sich auf ein `undefined` von `getContext` zu verlassen – das macht den Fehlerfall explizit statt still.

Zwei Fallstricke tauchen dabei mit schöner Regelmäßigkeit auf. Der erste: `setContext` muss synchron während der Initialisierung der Komponente aufgerufen werden. Wer den Aufruf in `onMount`, einen Event-Handler oder eine `async`-Funktion verschiebt, bekommt keinen Fehler, sondern etwas Schlimmeres – die Kinder erhalten beim späteren `getContext` schlicht `undefined`, und die Ursache ist schwer zu finden. Der zweite Fallstrick betrifft Reaktivität.

```mermaid
graph TD
  Provider["Toolbar<br/>setContext('toolbar', …)"]
  Group["ButtonGroup<br/>(keine Props)"]
  Divider["Divider<br/>(keine Props)"]
  Consumer["ToolbarButton<br/>getContext('toolbar')"]

  Provider --> Group --> Divider --> Consumer
```

### Context ist nicht dasselbe wie ein Store

Context ist an die Komponenteninstanz gebunden und für sich genommen nicht reaktiv. Lege ich einen einfachen Zahlenwert in den Context und ändere ihn später, aktualisieren sich die Kinder nicht – sie haben ihre Kopie beim Init gelesen, und dabei bleibt es. Ein Store dagegen ist global teilbar und reaktiv; mit `$store` abonniert man Änderungen und rendert bei jeder Aktualisierung neu. Über den Zustand außerhalb der Komponente und die Store-Mechanik habe ich in [Svelte Stores](https://www.mikebild.dev/de/blog/svelte-stores-zustand-ausserhalb-der-komponente/) ausführlicher geschrieben, und die zugrundeliegende Reaktivität ist Thema von [Svelte-Reaktivität](https://www.mikebild.dev/de/blog/svelte-reaktivitaet-compiler-ohne-virtual-dom/).

Die beiden schließen sich nicht aus, im Gegenteil. Die in der Praxis nützlichste Kombination legt einen Store *in* den Context. Damit bekommt man beides: die implizite Verteilung durch den Baum von Context und die Reaktivität eines Stores.

```svelte
<!-- ThemeProvider.svelte -->
<script>
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';

  const theme = writable('light');
  setContext('theme', theme);
</script>

<slot></slot>
```

```svelte
<!-- ThemeToggle.svelte -->
<script>
  import { getContext } from 'svelte';

  const theme = getContext('theme');
</script>

<button on:click={() => theme.update((t) => (t === 'light' ? 'dark' : 'light'))}>
  Current theme: {$theme}
</button>
```

Jetzt reicht Context den Store an jede Nachfahr-Komponente, und weil es ein Store ist, reagiert `{$theme}` auf jede Änderung – ohne ein einziges Prop durch den Baum zu fädeln.

## Web Components: framework-neutrale Wiederverwendung

Slots und Context lösen Kompositionsprobleme innerhalb einer Svelte-Anwendung. Das dritte Werkzeug zielt nach außen. Manchmal soll eine Komponente auch dort funktionieren, wo kein Svelte läuft – in einer bestehenden Seite, in einem anderen Framework, in reinem HTML. Svelte kann eine Komponente als Custom Element kompilieren, also als echtes Web Component nach Browser-Standard.

Zwei Dinge braucht es dafür. In der Komponente vergibt man über `<svelte:options>` einen Tag-Namen, und beim Kompilieren aktiviert man die Option `customElement`:

```svelte
<!-- UserBadge.svelte -->
<svelte:options tag="user-badge" />

<script>
  export let name = 'Unknown';
  export let status = 'offline';
</script>

<span class="badge" data-status={status}>
  {name}
</span>

<style>
  .badge {
    display: inline-flex;
    padding: 0.25rem 0.5rem;
    border-radius: 999px;
  }
  .badge[data-status='online'] {
    background: #d1fae5;
  }
</style>
```

Beim Bundling schaltet man den Compiler auf Custom-Element-Ausgabe. Mit Rollup und dem offiziellen Plugin sieht der relevante Teil so aus:

```js
// rollup.config.js
import svelte from 'rollup-plugin-svelte';

export default {
  // ...
  plugins: [
    svelte({
      compilerOptions: {
        customElement: true,
      },
    }),
  ],
};
```

Danach ist `<user-badge>` ein ganz normales HTML-Element. Es lässt sich in jeder Seite verwenden, ganz ohne Svelte-Laufzeit im Aufrufer:

```html
<script src="./user-badge.js"></script>

<user-badge name="Ada" status="online"></user-badge>
```

Die exportierten Props werden zu Element-Properties und -Attributen, sodass man das Element wie jedes andere DOM-Element ansprechen kann. Svelte kapselt die Styles per Shadow DOM – die Badge-Styles greifen nicht auf die umgebende Seite über, und Seiten-Styles dringen nicht unkontrolliert hinein. Das ist meist erwünscht, überrascht aber, wenn man globale Styles erwartet; `:global`-Regeln verhalten sich hier anders.

An dieser Stelle lohnt ein ehrlicher Blick auf die rauen Kanten, denn Ende 2022 ist der Custom-Element-Modus von Svelte funktional, aber nicht spannungsfrei. Der wichtigste Punkt betrifft die Datenübergabe: HTML-Attribute sind immer Strings. Ein `name="Ada"` im Markup kommt sauber an, aber ein Objekt oder ein Array lässt sich nicht als Attribut übergeben. Solche Werte muss man als JavaScript-Property auf dem Element setzen, etwa `el.items = [...]` – ein Detail, das man leicht übersieht, wenn man das Element nur deklarativ im HTML verwendet. Zweitens sind Custom Elements nach dem Definieren global registriert; derselbe Tag-Name lässt sich im selben Dokument nicht zweimal belegen, was beim Einbinden mehrerer Bundles zu Konflikten führen kann. Und drittens ist die Shadow-DOM-Kapselung zwar ein Gewinn für die Isolation, nimmt einem aber den bequemen Durchgriff von außen: Wer das Element von der Seite aus gestalten will, muss die dafür vorgesehenen Stellen selbst öffnen und CSS Custom Properties bewusst hineinreichen. Nichts davon ist ein Ausschlusskriterium, aber es sind die Stellen, an denen die Praxis von der ersten glatten Demo abweicht.

Ein praktischer Hinweis aus Projekten: `customElement: true` gehört nur auf die Komponenten, die man tatsächlich exportieren will, nicht projektweit. Sonst kompiliert man die ganze App zu Custom Elements und verliert den regulären Svelte-Komponentenmodus mit seinen Vorteilen. In der Praxis pflege ich dafür einen separaten Build-Eintrag, der genau die zu exportierenden Komponenten bündelt.

## Wann welches Werkzeug

Die drei Mechanismen lassen sich klar nach ihrem Zweck sortieren:

- Slots wählt man, wenn eine Komponente ihr Layout vorgibt, den Inhalt aber vom Aufrufer bekommen soll – mit slot props auch dann, wenn der Aufrufer für die Darstellung Daten aus dem Inneren braucht.
- Context wählt man, wenn ein Wert oder eine Funktion mehreren Ebenen tiefer benötigt wird und Prop-Drilling die Zwischenkomponenten unnötig belasten würde; für reaktive Werte legt man einen Store in den Context.
- Web Components wählt man, wenn eine Komponente außerhalb der Svelte-Welt laufen soll – in fremden Frameworks oder in reinem HTML.

Ein kurzer Hinweis zur Einordnung Ende 2022: Ich beziehe mich hier durchgehend auf Svelte 3, die aktuelle Version dieser Tage. Die gezeigte Syntax `tag="user-badge"` ist der Stand von Svelte 3; spätere Versionen haben an dieser Stelle Änderungen gebracht, die hier bewusst außen vor bleiben. Wer das Repository, an dem er arbeitet, auf Svelte 3 hält, ist mit allem hier Gezeigten auf sicherem Boden.

## Fazit

Slots, Context und Web Components wirken zunächst wie drei zusammenhanglose Features, sind aber ein zusammengehöriges Set von Kompositions-Werkzeugen, jedes mit einer eigenen Richtung. Slots reichen Markup von außen hinein und über slot props Daten von innen wieder heraus. Context verteilt Werte implizit durch den Komponentenbaum und erspart das Prop-Drilling – reaktiv wird es erst mit einem Store darin. Und Custom Elements lösen eine Komponente aus der Svelte-Welt heraus, sodass sie überall dort läuft, wo Standard-HTML läuft. Wer sich bei jedem Kompositionsproblem kurz fragt, in welche Richtung Inhalt oder Daten fließen sollen und wie weit sie reichen müssen, greift fast von selbst zum passenden der drei Werkzeuge. Diese eine Frage erspart im Projekt die meisten Umwege.

## Weiterführende Quellen

- Foliensatz zu diesem Thema: https://supabase.mikebild.dev/storage/v1/object/public/slides/introduction-svelte.html
- [Svelte 5: Snippets](https://svelte.dev/docs/svelte/snippet)
- [Svelte: Slots im Legacy-Modus](https://svelte.dev/docs/svelte/legacy-slots)
- [Svelte: Context-API](https://svelte.dev/docs/svelte/context)
- [Svelte: Custom Elements](https://svelte.dev/docs/svelte/custom-elements)
- [Svelte: `<svelte:options>`](https://svelte.dev/docs/svelte/svelte-options)
