Renderless Components und Headless Patterns in Vue.js
AI generated
<v/>
{ }
Vue.js · Renderless · Headless · Composition API
Renderless Components & Headless Patterns
Logik und Darstellung sauber trennen in Vue.js

Eine Komponente, die gleichzeitig Logik verwaltet und HTML rendert, lässt sich kaum wiederverwenden. Renderless Components und Headless Patterns in Vue.js lösen dieses Problem: Die Logik lebt in einem Composable oder einer renderless Komponente, die Darstellung übernimmt der Konsument vollständig per Scoped Slot oder Template.

14 Min. Lesezeit Renderless · Headless · Scoped Slots · Composables · Design Systems Vue 3 · Composition API · TypeScript

1. Was Renderless Components in Vue leisten

Das zentrale Problem in wachsenden Vue-Anwendungen ist die Kopplung von Logik und Darstellung in derselben Komponente. Ein Autocomplete-Component enthält sowohl die Such-Logik, die Tastaturnavigation, das Öffnen und Schließen des Dropdown und gleichzeitig das spezifische HTML-Markup mit seinen Design-Klassen. Das macht die Komponente nicht wiederverwendbar: Wer dasselbe Autocomplete-Verhalten mit anderem Markup braucht — in einem anderen Design-System, in einer anderen Farbe, als Mobile-optimierte Variante — muss die gesamte Komponente duplizieren oder in ein undurchsichtiges Props-Gebirge transformieren.

Renderless Components in Vue lösen dieses Problem durch radikale Trennung: Eine renderless Komponente rendert kein eigenes HTML, sondern stellt Logik, State und Aktionen via Scoped Slot bereit. Der Konsument entscheidet vollständig über das Markup. Das ist kein akademisches Konzept, sondern der Ansatz hinter Headless-UI-Bibliotheken wie Radix UI, Headless UI oder Vue's eigenem VueUse. Die Logik lebt einmal, die Darstellungen sind beliebig viele.

2. Das Renderless-Pattern: Komponente ohne eigenes Template

Eine renderless Komponente in Vue hat ein Template, das ausschließlich aus einem <slot> besteht — oder bei funktionaler Implementierung nur eine render-Funktion, die den Default-Slot mit State-Daten aufruft. Die gesamte Logik lebt in der setup-Funktion. State, computed Properties und Event-Handler werden als Slot-Props nach außen exponiert, damit der Konsument sie im eigenen Template verwenden kann. Das macht die renderless Komponente zu einem reinen Verhaltens-Provider ohne Meinung über die Darstellung.

Das Pattern ist in Vue 3 mit der Composition API besonders elegant, weil setup ohnehin alle reaktiven Daten zurückgibt. Eine renderless Komponente ist konzeptuell fast identisch mit einem Composable — der Unterschied liegt im Kontext: Das Composable wird in der setup-Funktion der Konsumentenkomponente eingebunden, während die renderless Komponente im Template des Konsumenten als Tag auftaucht und ihren State via Slot-Props teilt. Beide Muster haben ihre Berechtigung, je nachdem ob Template-Composition oder Code-Composition bevorzugt wird.


<!-- RenderlessAutocomplete.vue — Pure logic, no markup -->
<template>
  <!-- All state and actions exposed via scoped slot -->
  <slot
    :query="query"
    :results="results"
    :isOpen="isOpen"
    :isLoading="isLoading"
    :activeIndex="activeIndex"
    :onInput="handleInput"
    :onKeydown="handleKeydown"
    :selectItem="selectItem"
    :close="close"
  />
</template>

<script setup>
// Renderless component: only logic, no visual opinion
import { ref, watch } from 'vue'

const props = defineProps({
  modelValue: String,
  fetchFn: { type: Function, required: true },
  debounce: { type: Number, default: 300 },
})
const emit = defineEmits(['update:modelValue', 'select'])

const query = ref(props.modelValue ?? '')
const results = ref([])
const isOpen = ref(false)
const isLoading = ref(false)
const activeIndex = ref(-1)
let debounceTimer = null

const fetchResults = async (val) => {
  if (!val.trim()) { results.value = []; isOpen.value = false; return }
  isLoading.value = true
  try {
    results.value = await props.fetchFn(val)
    isOpen.value = results.value.length > 0
  } finally { isLoading.value = false }
}

const handleInput = (e) => {
  query.value = e.target.value
  clearTimeout(debounceTimer)
  debounceTimer = setTimeout(() => fetchResults(query.value), props.debounce)
}

const handleKeydown = (e) => {
  if (e.key === 'ArrowDown') activeIndex.value = Math.min(activeIndex.value + 1, results.value.length - 1)
  if (e.key === 'ArrowUp') activeIndex.value = Math.max(activeIndex.value - 1, 0)
  if (e.key === 'Enter' && activeIndex.value >= 0) selectItem(results.value[activeIndex.value])
  if (e.key === 'Escape') close()
}

const selectItem = (item) => { emit('select', item); emit('update:modelValue', item.label); close() }
const close = () => { isOpen.value = false; activeIndex.value = -1 }
</script>

3. Headless UI: Von der Komponente zum Konzept

Headless Patterns in Vue gehen einen Schritt weiter als einzelne renderless Komponenten: Sie beschreiben ein ganzes Konzept von Komponenten, die ausschließlich Verhalten und Zugänglichkeit liefern, niemals Styling. Die Idee entstammt der Beobachtung, dass Barrierefreiheit — korrekte ARIA-Attribute, Tastaturnavigation, Fokus-Management — für jede Art von Dropdown, Modal, Accordion oder Combobox identisch ist, egal wie sie aussieht. Diese Logik einmal korrekt zu implementieren und dann für beliebige visuelle Varianten wiederzuverwenden ist das Ziel des Headless-Ansatzes.

In Vue bedeutet das: Eine Headless-Disclosure-Komponente weiß, wann ein Bereich geöffnet oder geschlossen ist, setzt korrekte aria-expanded- und aria-controls-Attribute, handhabt Keyboard-Events und bietet einen Slot für den Trigger und einen für den Content. Was der Trigger visuell ist — ein Button, ein Link, ein Icon — entscheidet ausschließlich der Konsument. Die Headless-Komponente garantiert nur, dass es korrekt und zugänglich funktioniert.

4. Composable vs. Renderless Component: Wann welches Muster?

Die Frage, wann man ein Composable und wann eine renderless Komponente in Vue verwendet, ist eine Frage des Verwendungskontexts. Composables sind besser geeignet, wenn die Logik in bestehenden Komponenten eingebunden werden soll, ohne das Template zu verändern. Ein useAutocomplete-Composable kann in jede bestehende Komponente importiert werden, die bereits das richtige HTML-Markup hat. Die renderless Komponente ist besser geeignet, wenn die gesamte Komponentenhierarchie flexibel sein soll und der Konsument sein eigenes Template von Grund auf aufbaut.

Ein praktisches Kriterium: Wenn die Logik mehrere zusammengehörige Elemente steuert — zum Beispiel ein Trigger-Button und ein Content-Panel wie bei einem Accordion — ist die renderless Komponente eleganter, weil sie via Slot-Props beide Teile koordinieren kann. Wenn die Logik nur einen reaktiven State und ein paar Methoden umfasst — zum Beispiel eine Toggle-Funktion — reicht ein Composable vollständig aus. In modernen Vue-3-Projekten ist die Tendenz klar: Composables für reine Logik, renderless Komponenten wenn Template-Struktur koordiniert werden muss.


// composables/useDisclosure.js — Pure logic for open/close state
import { ref, computed } from 'vue'

export function useDisclosure(initialState = false) {
  const isOpen = ref(initialState)

  // Aria attributes computed — consumer just spreads them onto elements
  const triggerAttrs = computed(() => ({
    'aria-expanded': isOpen.value,
    'aria-controls': 'disclosure-panel',
  }))

  const panelAttrs = computed(() => ({
    id: 'disclosure-panel',
    'aria-hidden': !isOpen.value,
  }))

  const open = () => { isOpen.value = true }
  const close = () => { isOpen.value = false }
  const toggle = () => { isOpen.value = !isOpen.value }

  return { isOpen, open, close, toggle, triggerAttrs, panelAttrs }
}

// Usage in a component — full control over markup
// const { isOpen, toggle, triggerAttrs, panelAttrs } = useDisclosure()
// <button v-bind="triggerAttrs" @click="toggle">Toggle</button>
// <div v-bind="panelAttrs" v-show="isOpen"><slot /></div>

5. Scoped Slots als Kommunikationskanal

Scoped Slots sind das Rückgrat des Renderless-Patterns in Vue. Sie ermöglichen es einer Kind-Komponente, Daten in den Slot-Inhalt des Elternteils zu injizieren — umgekehrt zur normalen Datenflusskette in Vue. Die renderless Komponente bindet reaktive State-Werte und Methoden an den Slot, der Konsument empfängt sie als Template-Variablen und kann sie frei verwenden. Das ist mächtiger als Props, weil es volle Kontrolle über das Markup gibt, ohne eine feste Komponenten-API zu erzwingen.

Das Muster ist aber nicht unbegrenzt skalierbar. Wenn eine renderless Komponente mehr als zehn Slot-Props exponiert, wird die API unübersichtlich. Hier hilft eine Gruppierung: Statt zehn einzelner Props einen State-Objekt-Slot und einen Actions-Objekt-Slot exponieren. v-slot="{ state, actions }" ist lesbarer als v-slot="{ isOpen, query, activeIndex, results, onInput, onKeydown, selectItem, close, reset, isLoading }". Dieses Prinzip gilt auch für die TypeScript-Typisierung: Slot-Props sollten als Interface definiert sein, das der Konsument importieren und nutzen kann.

6. provide/inject für tief verschachtelte Headless-Bäume

Scoped Slots funktionieren gut, wenn die renderless Komponente direkt die Slot-Konsumenten steuert. Bei tief verschachtelten Komponentenbäumen — zum Beispiel einer Headless-Tabs-Komponente, die Tabs und Panels in beliebig tiefen Kind-Hierarchien verwaltet — werden Scoped Slots unpraktisch. Hier greift das provide/inject-Pattern in Vue, das für genau diesen Anwendungsfall konzipiert wurde: Eine übergeordnete Headless-Komponente stellt via provide einen geteilten State bereit, jede Kind-Komponente in der Hierarchie greift via inject darauf zu.

Das provide/inject-Muster ist typsicher mit einem InjectionKey-Symbol aus TypeScript: Die übergeordnete Headless-Komponente exportiert den Key, Kind-Komponenten importieren ihn und erhalten vollständige TypeScript-Autovervollständigung auf dem injizierten State. Zusammen mit readonly-Wrapping des bereitgestellten States — Kind-Komponenten können den State lesen, aber Mutationen nur via exponierte Methoden auslösen — ergibt sich ein sauberes unidirektionales Datenflussmuster auch in komplexen Headless-Hierarchien.

7. Headless in Design Systems: Stilfreiheit ohne Code-Duplikat

Der größte praktische Vorteil von Headless Patterns in Vue zeigt sich in Design Systems mit mehreren visuellen Varianten. Statt für jede Variante — Dark Mode, Mobile-Layout, kompaktes Dashboard-Layout — eine eigene Komponente mit duplizierter Logik zu pflegen, gibt es eine einzige Headless-Basiskomponente und beliebig viele Präsentationskomponenten darüber. Das bedeutet: Wenn ein Bug in der Tastaturnavigation eines Accordions gefunden wird, wird er einmal in der Headless-Basiskomponente behoben und alle Varianten profitieren sofort.

Bibliotheken wie Radix Vue oder Headless UI for Vue implementieren dieses Prinzip konsequent. In eigenen Design Systems folgt man demselben Muster: Headless-Komponenten in einem eigenen Paket oder Verzeichnis, Präsentationskomponenten als separate Wrapper, die ausschließlich Klassen und Markup definieren. Die Headless-Schicht ist stabil und getestet, die Präsentationsschicht ist leichtgewichtig und leicht austauschbar. Das ist das Fundament für wartbare, skalierbare Vue-Komponentenbibliotheken.


<!-- HeadlessTabs.vue — Logic and ARIA, no visual opinion -->
<template>
  <!-- Provide shared state to all child Tab and Panel components -->
  <slot :activeTab="activeTab" :setTab="setTab" />
</template>

<script setup>
import { ref, provide } from 'vue'
import { TABS_INJECTION_KEY } from './tabs-injection-key'

const props = defineProps({ defaultTab: { type: String, required: true } })
const activeTab = ref(props.defaultTab)
const setTab = (id) => { activeTab.value = id }

// Provide state to all descendant Tab/Panel components
provide(TABS_INJECTION_KEY, { activeTab, setTab })
</script>

<!-- DesignSystemTabs.vue — Visual wrapper using headless core -->
<template>
  <HeadlessTabs :defaultTab="defaultTab">
    <template #default="{ activeTab, setTab }">
      <div class="border-b border-slate-200 flex gap-0">
        <button
          v-for="tab in tabs"
          :key="tab.id"
          :aria-selected="activeTab === tab.id"
          :class="['px-4 py-2 text-sm font-medium border-b-2 transition-colors',
            activeTab === tab.id
              ? 'border-green-600 text-green-700'
              : 'border-transparent text-slate-500 hover:text-slate-700']"
          @click="setTab(tab.id)"
        >{ { tab.label } }</button>
      </div>
      <div class="mt-4">
        <slot :activeTab="activeTab" />
      </div>
    </template>
  </HeadlessTabs>
</template>

8. Renderless Components testen

Einer der größten Vorteile von Renderless Components in Vue ist die Testbarkeit. Da eine renderless Komponente kein Markup rendert, können Tests vollständig auf die Logik fokussieren, ohne sich um CSS-Klassen oder visuelle Details zu kümmern. Mit Vue Test Utils mounted man die renderless Komponente mit einem Test-Slot, der die Slot-Props in lokale Variablen aufnimmt. Dann werden Aktionen simuliert und der State überprüft — vollständig entkoppelt vom visuellen Rendering.

Der zweite Vorteil: Wenn die gesamte Logik in einem Composable liegt, kann das Composable direkt mit withSetup oder dem VueUse-Testpattern getestet werden, ohne eine Komponente zu mounten. Das macht Tests schneller und weniger fragil. Renderless Components und Headless Patterns sind deshalb nicht nur eine Architekturentscheidung für Wartbarkeit, sondern direkt mit der Qualität der Testabdeckung verknüpft — mehr Logik in Composables bedeutet einfachere, stabilere Tests.

9. Renderless vs. Headless vs. klassisches Component-Muster

Kriterium Klassisch Renderless Headless
Markup-Kontrolle Nur via Props Vollständig beim Konsumenten Vollständig beim Konsumenten
Wiederverwendbarkeit Niedrig Hoch Sehr hoch
Testbarkeit Mittel Hoch Sehr hoch
Einstiegshürde Niedrig Mittel Höher
Styling-Flexibilität Begrenzt Vollständig Vollständig
A11y-Garantie Manuell Manuell Eingebaut

Klassische Komponenten sind der richtige Ausgangspunkt für einfache, einmalig verwendete UI-Bausteine. Renderless Components lohnen sich, sobald dieselbe Logik in mehreren visuellen Varianten benötigt wird. Headless Patterns sind die Wahl für Komponentenbibliotheken und Design Systems, bei denen Zugänglichkeit und vollständige Styling-Kontrolle gleichzeitig gefragt sind. Die drei Muster schließen sich nicht aus — ein gut strukturiertes Vue-Projekt nutzt alle drei je nach Komplexität und Wiederverwendungsbedarf.

Mironsoft

Vue.js Komponentenarchitektur, Design Systems und Composable-Entwicklung

Vue-Komponentenbibliothek mit maximaler Wiederverwendbarkeit?

Wir entwickeln Renderless Components und Headless Patterns, die Logik und Darstellung sauber trennen — für wartbare Vue-Design-Systems mit vollständiger Styling-Kontrolle und A11y-Garantie.

Architektur-Review

Analyse bestehender Komponenten auf Logik-UI-Kopplung und Optimierungspotenzial

Headless-Entwicklung

Zugängliche Headless-Basiskomponenten für Accordion, Tabs, Dropdown, Combobox

Design-System-Integration

Präsentationsschicht als dünner Wrapper über Headless-Kern — stabil und austauschbar

10. Zusammenfassung

Renderless Components und Headless Patterns in Vue.js lösen das fundamentale Problem der Logik-UI-Kopplung. Eine renderless Komponente rendert kein eigenes Markup, stellt aber den vollständigen State und alle Aktionen via Scoped Slot bereit. Der Konsument entscheidet vollständig über das Aussehen. Headless Patterns erweitern diesen Ansatz auf ganze Komponentenhierarchien mit eingebauter Barrierefreiheit. Composables ergänzen das Bild: für reine Logik ohne Template-Koordination sind sie die leichtgewichtigste Variante.

Die Trennlinie zwischen den Mustern ist klar: Composable für isolierte Logik, renderless Komponente wenn Slot-Koordination nötig ist, Headless-Hierarchie wenn ganze Komponentenbäume koordiniert werden müssen. In Design Systems sind Headless Patterns der Schlüssel zu wartbaren Bibliotheken — ein Bug in der Tastaturnavigation wird einmal behoben und betrifft alle visuellen Varianten. Testbarkeit ist ein direkter Nebeneffekt: Logik in Composables und renderless Komponenten lässt sich ohne visuelle Tests vollständig abdecken.

Renderless & Headless in Vue — Das Wichtigste auf einen Blick

Renderless Component

Kein eigenes Markup. State und Aktionen via Scoped Slot. Konsument kontrolliert das Template vollständig.

Headless Pattern

Verhalten und A11y eingebaut, kein Styling. Beliebig viele Präsentationsschichten über einem Headless-Kern.

Composable

Reine Logik ohne Template. Leichtgewichtig, direkt in setup() integrierbar. Beste Testbarkeit.

provide/inject

Für tief verschachtelte Headless-Bäume. InjectionKey für TypeScript-Typsicherheit. Readonly-Wrapping verhindert direkte Mutationen.

11. FAQ: Renderless Components und Headless Patterns in Vue

1Was ist eine Renderless Component?
Kein eigenes HTML. State und Aktionen via Scoped Slot — der Konsument kontrolliert Markup und Styling vollständig.
2Renderless Component vs. Composable?
Composable in setup() für reine Logik. Renderless Komponente im Template wenn Slot-Koordination nötig ist.
3Was ist Headless UI in Vue?
Nur Verhalten und A11y, kein Styling. Beliebige Design-System-Varianten über einem stabilen Headless-Kern.
4provide/inject statt Scoped Slots?
Bei tief verschachtelten Bäumen. Kein Prop-Drilling über viele Ebenen — direkter Zugriff auf den Parent-State per inject.
5Renderless Components testen?
Test-Slot mounten, Slot-Props aufnehmen, Aktionen simulieren, State prüfen — vollständig unabhängig vom visuellen Rendering.
6Sind Renderless Components langsamer?
Nein — kein eigener DOM-Knoten ist oft sogar ein Vorteil. Kein messbarer Performance-Unterschied zu normalen Komponenten.
7Wie viele Slot-Props sind sinnvoll?
Maximal fünf bis sieben direkt. Mehr in Gruppen-Objekte fassen: v-slot="{ state, actions }" statt zehn einzelne Props.
8Fertige Headless-Bibliotheken für Vue?
Radix Vue, Headless UI for Vue, VueUse. Für eigene Design Systems eigene Headless-Schicht darüber empfohlen.
9Renderless Components in TypeScript typisieren?
Slot-Props als Interface exportieren. InjectionKeys mit generischen Typen für provide/inject. Konsumenten bekommen volle Autovervollständigung.
10Renderless mit Tailwind CSS kombinieren?
Perfekte Kombination. Renderless liefert Logik und ARIA, der Konsument baut das Tailwind-Markup — verschiedene Design-Varianten, eine Logik.