Props & Slots in Vue

Die richtige API für professionelles Komponenten-Design wählen

Ein praxisnaher Vergleich von Vue.js Props und Slots mit Entscheidungshilfe und Pattern aus der Praxis

2026/01/29

Hintergrund

Komponentenbasierte Frameworks sind heute de facto Industriestandard in der Softwareentwicklung. Komponenten strukturieren das User Interface in klar abgegrenzte Bereiche und erhöhen damit die Wiederverwendbarkeit von Code erheblich. Gleichzeitig wird die Frontend-Architektur langfristig wartbarer.

Ändern sich Anforderungen an das Layout, müssen Anpassungen in der Regel nur an einer Stelle erfolgen: in der jeweiligen Komponente selbst. Komponenten kapseln Darstellung und zugehörige Logik und ermöglichen damit auch im Frontend eine saubere Separation of Concerns.

Das Komponenten-Konzept findet sich deshalb in nahezu allen modernen Frontend- und Fullstack-Frameworks wieder. Bekannte Vertreter sind Vue.js, React, Angular, Svelte, Laravel mit Blade oder auch Freemarker. Nicht alle Lösungen bieten jedoch dieselben APIs oder den gleichen Grad an Flexibilität.

Dieser Artikel fokussiert sich auf die Konzeption von Komponenten in Vue.js.

Individualisierbarkeit meistern

Je häufiger Komponenten eingesetzt werden, desto wichtiger werden ihre Schnittstellen. Besonders bei der Umsetzung von Design-Systemen oder beim Aufbau von UI-Libraries für mehrere Projekte sind diese Entscheidungen langfristig relevant. Falsch gewählte APIs lassen sich später meist nur durch umfangreiches (und teures) Refactoring korrigieren.

Von Anfang an die richtigen Schnittstellen zu definieren, ist daher ein entscheidender Faktor für professionelle Frontend-Architektur.

Die zentrale Frage beim Entwurf von Komponenten lautet: Wie fließen Daten zwischen den Komponenten?

Für optimale Wiederverwendbarkeit müssen dieselben Komponenten sehr unterschiedliche Anforderungen erfüllen können. Eine Button-Komponente benötigt wechselnde Labels, eine Link-Komponente unterschiedliche URLs, und eine Card-Komponente zeigt in der Regel nie zweimal exakt denselben Inhalt.

Der Datenfluss ist dabei meist bidirektional: von Parent- zu Child-Komponenten (etwa über props) und von Child- zu Parent-Komponenten (z. B. über Events). Der Datenfluss von oben nach unten existiert jedoch nahezu immer. In Vue.js stehen dafür zwei APIs zur Verfügung: props und slots.

Props

props sind der üblichste Weg Daten an eine Vue-Komponente zu übergeben. In Ihrer Syntax ähneln sie HTML-Attributen. Mit Data-Bindung (z.B. :title oder v-bind:title) können auch reaktive Variablen übergeben werden. Reaktiv bedeutet, dass die Veränderung des Wertes dieser Variable Reaktionen nach sich zieht, ohne diese explizit auszulösen, wie etwa die automatische Aktualisierung des UI, die Aktualisierung von computed properties usw. Innerhalb der Komponente sind Properties dann innerhalb der Logik verfügbar (also im <script>).

props lassen sich mit TypeScript typisieren. Sie können als optional definiert werden (mit ?) und Standardwerte haben (mit withDefaults)

Folgendes Beispiel einer Card-Komponente illustriert die Verwendung von props.

props sind der klassische und am häufigsten genutzte Weg, um Daten an eine Vue-Komponente zu übergeben. In ihrer Syntax ähneln sie HTML-Attributen. Über Data Binding (z. B. :title oder v-bind:title) lassen sich auch reaktive Variablen übergeben.

Reaktiv bedeutet dabei, dass Änderungen an diesen Variablen automatisch Folgeaktionen auslösen – etwa eine Aktualisierung des UI oder von computed Properties.

Innerhalb der Komponente stehen props sowohl im <template> als auch in der Logik (<script>) zur Verfügung.

props lassen sich mit TypeScript typisieren, als optional definieren (mit ?) und mit Standardwerten versehen (z. B. über withDefaults).

Das folgende Beispiel zeigt eine einfache Card-Komponente mit props:

        <!-- Card.vue -->

<template>
  <div class="shadow-xl flex flex-col gap-5 border border-gray-300 p-4 rounded-lg">
    <h3 v-if="title">{{ title }}</h3>
    <p>
      {{ content }}
    </p>
  </div>
</template>

<script setup lang="ts">
  defineProps<{
    title?: string;
    content: string;
  }>()
</script>

      

Die Komponente definiert zwei Props (title und content), die über Template-Bindings im UI ausgegeben werden.

Übergeben werden können die Props entweder statisch oder reaktiv:

        <!-- App.vue -->

<!-- not reactive -->
<Card title="Hello world!" content="Lorem Ipsum" />

<!-- reactive -->
<Card :title="cardTitle" :content="cardContent" />

<script setup lang="ts">
  import { ref } from 'vue';

  const cardTitle = ref('John Doe');
  const cardContent = ref('Turn visitors into customers with a clear message here.');
</script>

      

Im reaktiven Szenario aktualisiert sich das UI automatisch, sobald sich cardTitle oder cardContent ändern.

Da Props auch im <script> verfügbar sind, können sie dort weiterverarbeitet werden:

        <!-- Card.vue -->

<script setup lang="ts">
  import { computed } from 'vue';

  const props = defineProps<{
    title?: string;
    content: string;
  }>()

  const titleWithEmoji = computed(() => `${ props.title } 🚀`);
</script>

      

In diesem Beispiel aktualisiert sich titleWithEmoji immer dann, wenn sich der Wert von props.title ändert.

Die Grenze von Props

Props eignen sich hervorragend, um Komponenten zu konfigurieren oder ihnen einfache Daten zu übergeben.

Unsere <Card />-Komponente ist zwar hinsichtlich ihrer Inhalte dynamisch, legt ihre Präsentation jedoch strikt fest. Sie ist also bewusst opinionated.

Problematisch wird das insbesondere bei props wie content. Da es sich um einen einfachen string handelt, ist die Darstellung komplexerer Inhalte – etwa Buttons, Icons oder Links – nur über Umwege möglich (z. B. über VNodes).

        <!-- App.vue -->

<!-- undesirable / broke -->
<Card title="Hello world!" content="<b>Lorem</b> Ipsum" />

      

Konzeptionell erfüllt <Card /> eine reine Layout-Funktion. Der Komponente ist es "egal" was in ihr dargestellt wird.

Merksatz: props eignen sich besser zur Konfiguration als zur freien Visualisierung.

An dieser Stelle kommen slots ins Spiel.

Slots

Während props mit HTML-Attributen vergleichbar sind, entsprechen slots eher den Kind-Elementen eines HTML-Tags.

Der Inhalt eines Slots ist nicht in derselben Einfachheit im <script> verfügbar wie Props. Dafür ist aber auch keine explizite Deklaration notwendig. Es reicgenügtht, im <template> festzulegen, wo der Slot gerendert wird. Die Komponente is agnostisch darüber, was im slot gerendert wird, also hinsichtlich des Inhalts "unopinionated".

        <template>
  <div class="shadow-xl flex flex-col gap-5 border border-gray-300 p-4 rounded-lg">
    <h3 v-if="title">{{ title }}</h3>
    <p>
     <slot />
    </p>
  </div>
</template>

<script setup lang="ts">
  defineProps<{
    title?: string;
  }>()
</script>

      

Die Deklaration von content als prop kann in diesem Beispiel entfallen. Ab jetzt ist es für <Card /> unerheblich was der Inhalt des <p> ist. Dies können nun mehrere Variablen, HTML-Elemente oder sogar weitere Vue-Komponenten sein, wie der <Hyperlink /> im unteren Beispiel

        <!-- Hyperlink.vue -->
<template>
  <a :href="href" class="text-blue-500 hover:text-blue-700">
    <slot />
  </a>
</template>

<script setup lang="ts">
  defineProps<{
    href: string;
  }>()
</script>

      
        <!-- App.vue -->

<Card :title="cardTitle">
  {{ cardContent }}
  <Hyperlink href="https//nils-siemsen.de">
    My Homepage
  </Hyperlink>
</Card>

      

Die Reaktivität der Variable cardContent bleibt hierbei erhalten. Durch Nutzung eines slot wird nun alles innerhalb des öffnenden und schließenden Tags von <Card></Card> genau dort ausgegeben, wo innerhalb von <Card /> das <slot /> Element platziert wurde. Damit können wir nun auch einen Hyperlink in der Karte platzierern, ohne zusätzlichen Konfigurationsaufwand über weitere props von <Card /> (bspw. durch eine prop "href") zu erzeugen.

Grundsätzlich lässt sich ein <slot /> beliebig oft platieren. Durch Benennung von slots ist außerdem die Nutzung mehrerer slots in derselben Komponente möglich.

        <!-- Card.vue -->

<template>
  <div class="shadow-xl flex flex-col gap-5 border border-gray-300 p-4 rounded-lg">
    <h3 v-if="$slots.title">
      <slot name="title" />
    </h3>
    <p>
      <slot />
    </p>
  </div>
</template>

      

Wie bei props lässt sich auch bei slots prüfen, ob sie belegt sind ($slots) und entsprechendes konditionales Rendering umsetzen.

Die Grenze von Slots

Slots sind kein Allheilmittel. Am Beispiel einer <Hyperlink />-Komponente wird schnell klar, warum die korrekte Auswahl der API entscheidend ist.

Stellen wir uns einmal vor <Hyperlink /> würde wie <Card /> alle props durch slots ersetzen:

        <!-- Hyperlink.vue-->
<template>
  <a :href="$slots.default" class="text-blue-500 hover:text-blue-700">
    <slot />
  </a>
</template>

      

Es wird versucht den Inhalt des slot über Datenbindung mit : and das href des <a> Elements zu binden. Dies führt zu folgendem Ergebnis:

        <a href="[object Object]" class="text-blue-500 hover:text-blue-700"> My Homepage </a>

      

Der Vue Compiler wird außerdem diese Warnung ausgeben:

        Type 'VNode<RendererNode, RendererElement, { [key: string]: any; }>[] | undefined' is not assignable to type 'string | undefined'.

      

Die Warnung erklärt auch exakt warum :href="$slots.default" zu href="[object Object]" wird: Ein Slot hat als Wert immer ein Array aus VNode. Dieses Nodes sind interne Abstraktionen von Vue.js. Es lässt sich zwar auf den textuellen Inhalt eines VNode zugreifen, dieser Weg ist aber deutlich komplexer als einfach props zu nutzen.

Merksatz: Slots eignen sich besser zur Visualisierung als zur Konfiguration.

Die Entscheidung

Das Ziel guter Komponenten ist maximale Wiederverwendbarkeit – idealerweise über mehrere Produkte hinweg. Jede kluge Entscheidung beim API-Design zahlt sich langfristig aus. Jede falsche Entscheidung erhöht die Kosten späterer Refactorings exponentiell für alle Produkte, die diese Komponente einbinden.

Komponenten können nur dann vielseitig eingesetzt werden, wenn sie an klar definierten Stellen über Props oder Slots individualisierbar sind. Beide APIs schließen sich nicht aus, sondern ergänzen sich:

  • props steuern Verhalten und Konfiguration
  • slots ermöglichen flexible Darstellung

Da mit steigender Anzahl an Nutzern einer Komponente nicht nur der Nutzen ihrer Entwicklung sondern auch die Anzahl an Use Cases steigt empfehle ich slots allgemein den Vorzug zu geben, solange ihr Inhalt nicht von der Logik einer Komponente benötigt wird.

Beispiel gefällig?: Es gibt konzeptionell quasi keinen Grund folgendes zu tun:

        <Card variant="large" title="Hello world!" content="Lorem Ipsum" />

      

Man beachte variant, was eine Design-Variante der Karte definieren könnte, die Visualisierug der übrigen Daten aber den slots überlässt

Es gibt hingegen theoretisch unendlich viele Gründe folgendes zu tun:

        <Card variant="large">
  <template #title>Hello World!</template>
  Lorem Ipsum
</Card>

      

Denn jeder Nutzer der Komponente kann genau das in der Komponente darstellen, was die entsprechende Anforderung erfordert.

Meine Empfehlung lautet also:

Nutze Slots für visuelle Inhalte, die sich stark unterscheiden können. Nutze Props für Daten, die das Verhalten oder die Struktur einer Komponente beeinflussen.

Vorsicht vor Flexibilität

Denkbar sind aber spezifische Fälle, die props vor slots den Vorzug geben könnten. Es kann nämlich in Szenarien, in denen mehrere unabhängige Teams mit denselben Komponenten arbeiten durchaus sinn ergeben, die Visualisierung durch Verzicht auf props weniger dynamisch zu gestalten.

Stellen wir uns vor wir gestalten in unserer <Card /> Komponente noch gezielt den Inhalt das <p>, weil unser Design-System vorgibt, dass jeder Text in dieser Karte zwingend die gleiche Schrift haben muss.

        <!-- Card.vue -->

<template>
  <div class="shadow-xl flex flex-col gap-5 border border-gray-300 p-4 rounded-lg">
    <h3 v-if="$slots.title">
      <slot name="title" />
    </h3>
    <p class="text-base text-gray-900 font-regular">
      <slot />
    </p>
  </div>
</template>

      

Verwenden wir nun slots ergibt sich folgendes Problem:

        <!-- App.vue -->

<template>
  <Card>
    <span class="text-2xl font-bold text-green-200">I solemnly swear that I am up to no good.</span>
  </Card>
</template>

      

Es gibt technisch nichts, das den Nutzer davon abhält, den Text einfach nach eigenem Ermessen zu gestalten. Da bei CSS die Anweisungen auf Kind-Elementen die Anweisungen auf Eltern-Elementen gleicher Spezifität überschreiben, ist ein Abweichen vom UX-Konzept problemlos möglich.

Dieses Problem lösen wir, indem wir slots durch props ersetzen.

        <!-- App.vue -->

<template>
  <Card :contenr="Crap. You got me" />
</template>

      

Eine Umgestaltung des Inhalts ist dann nicht mehr möglich.

Hier wird nun also die Individualisierbarkeit bewusst eingeschränkt, um globale Konsistenz sicherzustellen.

Die Entscheidungshilfe lässt sich somit erweitern:

Nutze dann props für visuell dargestellte Inhalte, wenn ein striktes Design-System auch bei Drittparteien sichergestellt werden muss.

Dieses Problem kommt aber erst dann auf, wenn Komponenten über Team- oder Unternehemensgrenzen hinaus geteilt werden und ist damit nur eine Frage für spezifische Enterprise-Projekte. Der Vorzug von slots zur visuellen Darstellung bleibt damit für mich erhalten.

Bedenke allerdings, dass slots für Drittparteien quasi der Todesstern sind, um Design-Systeme zu zerstören. Vertrauen muss da sein.

Bonus: Headless Components

Zum Abschluss möchte ich ein Design Pattern vorstellen, das zeigt, wie auch slots mit der internen Logik einer Komponente interagieren können.

Headless Components sind deswewegen "headless", weil der "head", also die Darstellung, komplett fehlt oder ersetzt werden kann. Komponenten dieser Art kapseln also nur Logik und machen höchstens einen Visualisierungsvorschlag, der durch einen slot aber komplett (neu) definiert werden kann.

Das funktioniert, weil slots über scoped slots auf die Logik einer Komponente zugreifen können.

Stellen wir uns eine semantische Komponente für einen Blog vor: <Posts />. Sie hat die Aufgabe eine Menge an Blog-Artikeln als Liste anzuzeigen.

Sie gibt eine Liste an posts mit v-for aus:

          <Card v-for="post in posts">
      <template #title>
          {{ post.title }} [{{ post.id }}]
      </template>
      <div class="flex flex-col">
          <i class="mb-3">by user {{ post.userId }}</i>
          <p>
              {{ post.body }}
          </p>
      </div>
  </Card>

      

Nachdem sie von einer API geladen wurden:

          onMounted(async () => {
    posts.value = await fetch(props.url)
        .then(res => res.json())
        .catch(console.error);
  })

      

Stellen wir nun posts über einen scoped slot zur Verfügung:

        <!-- Posts.vue -->

<slot :posts="posts">

      

Dann kann die Parent-Komponente in diesem Slot die Darstellung von reaktiven Daten aus Child-Komponente völlig frei definieren:

        <!-- App.vue -->

  <Posts>
    <template #default="{ posts }">
      <nav class="grid grid-cols-1">
        <Hyperlink v-for="post in posts" :href="`https://jsonplaceholder.typicode.com/posts/${ post.id }`">
          {{ post.title }}
        </Hyperlink>
      </nav>
    </template>
  </Posts>

      

Dabei bleibt die volle Reaktivität von posts erhalten. Dieses Pattern erlaubt es somit auch Filter-, Such- und Paginierungs-logik einmalig zu implementieren und in beliebig vielen Visualisierungsformen zu nutzen.

Entdeckt gerne in meinem dazu passenden Github-Repository die verschiedenen Anwendungsbereiche von props und slots!

Hi, ich bin Nils!

👋 Hey, ich bin Nils — Fullstack-Entwickler, KI-Enthusiast und kreativer Problemlöser aus Ulm mit einer Leidenschaft für intelligente Lösungen, sauberen Code und großartige Ideen. Ich setze ganze Produkte in Frontend, Backend, Architektur um - von der Idee bis zum Deployment 🚀.
sketch of Nils Siemsen in casual style