von starren Media Queries zu adaptiven Komponenten
Responsive Vue Komponenten scheitern oft an reinen CSS-Media-Queries, weil sich Komponenten in unterschiedlichen Layout-Kontexten unterschiedlich verhalten müssen, unabhängig von der Viewport-Breite. Container Queries, eigene Breakpoint-Composables und adaptive Slot-Strukturen lösen das strukturell statt mit immer mehr CSS-Ausnahmen.
Inhaltsverzeichnis
- 1. Warum reine Media Queries für Komponenten nicht reichen
- 2. Ein Breakpoint-Composable als zentrale Wahrheitsquelle
- 3. Container Queries: Layout abhängig vom Elternelement
- 4. Adaptive Slots: unterschiedliche Inhalte je Breakpoint
- 5. Conditional Rendering statt reinem CSS-Verstecken
- 6. Praxisbeispiel: Navigation, die sich strukturell wandelt
- 7. Praxisbeispiel: Tabellen zu Karten auf kleinen Bildschirmen
- 8. Responsive Komponenten testen ohne echten Browser-Resize
- 9. Ansätze im Vergleich: Media Query, Container Query, JS-Breakpoint
- 10. Zusammenfassung
- 11. FAQ
1. Warum reine Media Queries für Komponenten nicht reichen
Responsive Vue Komponenten werden häufig ausschließlich mit CSS-Media-Queries umgesetzt, die auf die Breite des Viewports reagieren. Das funktioniert gut für Seiten-Layouts, versagt aber, sobald eine Komponente in unterschiedlichen Kontexten wiederverwendet wird, etwa eine Karte, die einmal in einer schmalen Sidebar und einmal im breiten Hauptbereich steht. Eine Media Query kennt nur die Viewport-Breite, nicht die tatsächlich verfügbare Breite der Komponente selbst.
Das Grundproblem bei responsiven Vue Komponenten ist also eine Verwechslung von zwei unterschiedlichen Fragen: Wie groß ist der Bildschirm, und wie viel Platz hat diese eine Komponente gerade zur Verfügung. Für globale Layout-Entscheidungen wie das Ein- oder Ausblenden einer Sidebar ist die erste Frage richtig. Für das Verhalten einer wiederverwendbaren Komponente, die in Grids, Sidebars oder Modals unterschiedlich platziert wird, ist die zweite Frage die eigentlich relevante.
Vue-Komponenten sind von Natur aus wiederverwendbare Bausteine, die in unterschiedlichen Layout-Kontexten landen. Genau deshalb lohnt es sich, für responsive Vue Komponenten mehrere Werkzeuge kombiniert einzusetzen: JavaScript-basierte Breakpoint-Composables für strukturelle Entscheidungen, die über reines CSS hinausgehen, und Container Queries für Komponenten, die unabhängig vom Viewport auf ihren eigenen verfügbaren Platz reagieren müssen.
2. Ein Breakpoint-Composable als zentrale Wahrheitsquelle
Sobald eine Komponente nicht nur ihr Aussehen, sondern ihre Struktur ändern soll, etwa eine Navigation, die auf Mobilgeräten komplett anders aufgebaut ist als auf Desktop, reicht CSS allein nicht mehr aus. Ein zentrales Breakpoint-Composable liefert reaktive Boolean-Werte, auf die jede Komponente im Projekt zugreifen kann, ohne die Breakpoint-Logik an mehreren Stellen zu duplizieren.
Die VueUse-Bibliothek stellt mit useBreakpoints bereits eine ausgereifte Basis bereit, die sich um ein zentrales Composable herum kapseln lässt, damit alle Komponenten dieselben Breakpoint-Definitionen verwenden wie das Tailwind-Konfigurationsschema. Diese Konsistenz zwischen CSS-Breakpoints und JavaScript-Breakpoints ist entscheidend, weil abweichende Werte zu Layout-Sprüngen führen, bei denen CSS und JavaScript unterschiedliche Entscheidungen treffen.
// composables/useAppBreakpoints.js
import { useBreakpoints } from '@vueuse/core'
// Mirror Tailwind's default breakpoint scale for consistency
const breakpoints = useBreakpoints({
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
})
export function useAppBreakpoints() {
const isMobile = breakpoints.smaller('md')
const isTablet = breakpoints.between('md', 'lg')
const isDesktop = breakpoints.greaterOrEqual('lg')
const active = breakpoints.active()
return { isMobile, isTablet, isDesktop, active }
}
// components/NavigationRoot.vue — script setup block
import { useAppBreakpoints } from '@/composables/useAppBreakpoints'
import MobileNav from './MobileNav.vue'
import DesktopNav from './DesktopNav.vue'
const { isMobile } = useAppBreakpoints()
// template:
// <MobileNav v-if="isMobile" />
// <DesktopNav v-else />
Der Vorteil dieses Ansatzes gegenüber CSS-Verstecken mit hidden md:block: Bei responsiven Vue Komponenten mit strukturell unterschiedlichem Markup, etwa einer komplett anderen Navigationslogik für Mobile, werden beide Varianten nicht gleichzeitig ins DOM gerendert. Das spart unnötige DOM-Knoten, verhindert doppelte Event-Listener und macht Screenreadern klar, welche Version tatsächlich aktiv ist, statt beide Varianten per aria-hidden zu verstecken.
3. Container Queries: Layout abhängig vom Elternelement
Container Queries lösen genau das Problem, das reine Viewport-Media-Queries nicht lösen können: Eine Komponente reagiert auf die Breite ihres eigenen Containers, unabhängig davon, wie breit der gesamte Bildschirm ist. Für responsive Vue Komponenten, die in unterschiedlichen Kontexten wiederverwendet werden, sind Container Queries damit oft die treffendere Lösung als globale Breakpoints.
Technisch wird ein Element mit container-type: inline-size zu einem Query-Container erklärt, und Kindelemente können dann mit @container-Regeln auf dessen Breite reagieren, nicht auf die des Viewports. In Tailwind CSS 4 ist diese Funktionalität über die @container-Klasse und Prefixe wie @sm: oder @lg: direkt nutzbar, ohne zusätzliches Plugin.
<!-- components/StatCard.vue -->
<template>
<!-- The card itself becomes the query container -->
<div class="@container rounded-xl border border-slate-200 p-4">
<!-- Layout adapts to the CARD's own width, not the viewport -->
<div class="flex flex-col @sm:flex-row @sm:items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
<slot name="icon" />
</div>
<div>
<p class="text-xs text-slate-500 @sm:text-sm">{{ label }}</p>
<p class="text-xl @sm:text-2xl font-bold text-slate-900">{{ value }}</p>
</div>
</div>
</div>
</template>
Dieselbe StatCard-Komponente stapelt Icon und Text vertikal, wenn sie in einer schmalen Sidebar mit 200 Pixel Breite eingebettet ist, und zeigt sie nebeneinander, wenn sie in einem breiten Dashboard-Grid mit 400 Pixel Spaltenbreite landet, ganz ohne dass die Komponente selbst weiß, in welchem Kontext sie gerade läuft. Das ist der entscheidende Vorteil von Container Queries gegenüber Media Queries für responsive Vue Komponenten: echte, kontextunabhängige Wiederverwendbarkeit.
4. Adaptive Slots: unterschiedliche Inhalte je Breakpoint
Manche responsive Vue Komponenten müssen nicht nur ihr Layout, sondern ihren tatsächlichen Inhalt anpassen, etwa eine gekürzte Beschreibung auf Mobilgeräten gegenüber einer ausführlichen Version auf Desktop. Scoped Slots kombiniert mit einem Breakpoint-Composable erlauben es der aufrufenden Komponente, unterschiedliche Inhalte für unterschiedliche Bildschirmgrößen bereitzustellen, ohne die Basis-Komponente selbst mit Konditional-Logik zu überladen.
Dieses Pattern funktioniert besonders gut bei generischen Layout-Komponenten wie Karten oder Listen-Items, die von vielen unterschiedlichen Stellen im Projekt mit unterschiedlichem Detailgrad befüllt werden sollen. Die Basis-Komponente exponiert lediglich den aktuellen Breakpoint-Status über einen Scoped Slot, die Entscheidung über den konkreten Inhalt bleibt beim Aufrufer.
<!-- components/ResponsiveSlot.vue — exposes breakpoint state to its slot -->
<script setup>
import { useAppBreakpoints } from '@/composables/useAppBreakpoints'
const { isMobile, isTablet, isDesktop } = useAppBreakpoints()
</script>
<template>
<slot :isMobile="isMobile" :isTablet="isTablet" :isDesktop="isDesktop" />
</template>
<!-- usage in a parent component -->
<ResponsiveSlot v-slot="{ isMobile }">
<p v-if="isMobile">{{ product.shortDescription }}</p>
<p v-else>{{ product.fullDescription }}</p>
</ResponsiveSlot>
Ein solches Wrapper-Pattern hält die eigentliche Breakpoint-Logik an einer einzigen Stelle, während beliebig viele Stellen im Projekt davon profitieren, ohne useAppBreakpoints jedes Mal selbst zu importieren. Für sehr einfache Fälle reicht der direkte Import des Composables völlig aus, für breit wiederverwendete, generische Basis-Komponenten lohnt sich der Scoped-Slot-Ansatz, weil er die Verantwortung für den konkreten Inhalt klar beim Aufrufer belässt.
5. Conditional Rendering statt reinem CSS-Verstecken
Ein häufiger Anti-Pattern bei responsiven Vue Komponenten: Zwei komplette Varianten einer Komponente werden gleichzeitig ins Markup geschrieben, eine davon mit hidden md:block versteckt. Für einfache Sichtbarkeits-Unterschiede, etwa ein zusätzliches Icon nur auf Desktop, ist das akzeptabel. Für strukturell komplett unterschiedliche Komponenten mit eigenen Event-Handlern, eigenem State und eigenen API-Aufrufen ist das jedoch teuer: Beide Varianten werden gemountet, beide Varianten führen ihre onMounted-Hooks aus, beide Varianten registrieren Event-Listener.
Die Lösung ist konsequentes Conditional Rendering mit v-if/v-else in Kombination mit dem Breakpoint-Composable, statt v-show oder CSS-Klassen. v-if entfernt die nicht benötigte Variante vollständig aus dem DOM und dem Component-Tree, sodass Lifecycle-Hooks nur für die tatsächlich aktive Variante laufen. Bei responsiven Vue Komponenten mit teuren Kindkomponenten, etwa Chart-Bibliotheken oder Kartenkomponenten, spart das spürbar Initialisierungszeit und Speicher.
// AVOID: both variants mount, both run lifecycle hooks and fetch data
// <DesktopChart class="hidden md:block" />
// <MobileChart class="md:hidden" />
// BETTER: only the active variant mounts and initializes
// <DesktopChart v-if="isDesktop" />
// <MobileChart v-else />
// Composable stays the single source of truth for both cases
import { useAppBreakpoints } from '@/composables/useAppBreakpoints'
const { isDesktop } = useAppBreakpoints()
6. Praxisbeispiel: Navigation, die sich strukturell wandelt
Eine Hauptnavigation ist das klassische Beispiel für responsive Vue Komponenten, bei denen sich nicht nur das Aussehen, sondern die gesamte Interaktionslogik ändert. Auf Desktop ist eine horizontale Leiste mit Dropdown-Menüs bei Hover sinnvoll, auf Mobilgeräten braucht es stattdessen ein Off-Canvas-Menü mit Touch-freundlichen, größeren Tap-Zielen und ausklappbaren Untermenüs statt Hover-Dropdowns.
Der saubere Ansatz kombiniert das Breakpoint-Composable aus Abschnitt zwei mit zwei komplett getrennten Komponenten, die sich dieselbe Datenquelle für die Navigationsstruktur teilen. Die eigentlichen Navigationsdaten, also Labels, Links und Untermenüs, kommen aus einer gemeinsamen Konfiguration oder einem Composable, während DesktopNav und MobileNav jeweils ihre eigene, für den Kontext passende Interaktionslogik implementieren.
// composables/useNavigation.js — shared data source for both nav variants
export function useNavigation() {
const items = [
{ label: 'Produkte', to: '/products', children: [
{ label: 'Elektronik', to: '/products/electronics' },
{ label: 'Kleidung', to: '/products/clothing' },
]},
{ label: 'Über uns', to: '/about' },
{ label: 'Kontakt', to: '/contact' },
]
return { items }
}
// DesktopNav.vue uses hover-triggered dropdowns for `items`
// MobileNav.vue uses an off-canvas panel with expandable accordions for `items`
// Both read from the same useNavigation() composable, avoiding duplicated data
Diese Trennung verhindert, dass Navigationsdaten an zwei Stellen gepflegt werden müssen, während gleichzeitig jede Variante ihre eigene, für den jeweiligen Bildschirmkontext optimierte Interaktionslogik behält. Ein häufiger Fehler ist der Versuch, eine einzige Navigationskomponente mit unzähligen bedingten Klassen für beide Fälle zu bauen, was schnell unübersichtlich wird, sobald Hover-Zustände und Touch-Gesten gleichzeitig unterstützt werden müssen.
7. Praxisbeispiel: Tabellen zu Karten auf kleinen Bildschirmen
Datentabellen sind ein weiterer Klassiker unter den responsiven Vue Komponenten, weil eine Tabelle mit vielen Spalten auf einem schmalen Mobilbildschirm entweder horizontal gescrollt werden muss oder komplett in ein anderes Layout wechseln sollte. Die gängige Lösung wandelt jede Tabellenzeile auf kleinen Bildschirmen in eine Karte um, in der jede Spalte als Label-Wert-Paar erscheint, statt als Tabellenzelle.
Technisch lässt sich das elegant mit derselben Datenstruktur für beide Darstellungen lösen: Eine gemeinsame Spalten-Konfiguration definiert Label und Wert-Zugriff pro Spalte, und je nach Breakpoint rendert die Komponente entweder eine table-Struktur oder eine Liste von Karten, die dieselbe Spalten-Konfiguration durchläuft.
// composables/useColumns.js — shared column config for table AND card views
export function useProductColumns() {
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'sku', label: 'Artikelnummer' },
{ key: 'stock', label: 'Bestand' },
{ key: 'price', label: 'Preis' },
]
return { columns }
}
// AdaptiveTable.vue — script setup block
import { useAppBreakpoints } from '@/composables/useAppBreakpoints'
import { useProductColumns } from '@/composables/useColumns'
const { isMobile } = useAppBreakpoints()
const { columns } = useProductColumns()
defineProps({ rows: { type: Array, required: true } })
// template (desktop): a plain <table> iterating `columns` per <th>/<td>
// template (mobile, v-if="isMobile"): one card per row, each column
// rendered as a label/value pair using the same `columns` array
Der entscheidende Vorteil dieses Musters: Ändert sich die Spaltenstruktur, etwa eine neue Spalte für Lagerort, muss sie nur einmal in useProductColumns ergänzt werden, und sowohl Tabellen- als auch Karten-Darstellung übernehmen die Änderung automatisch. Ohne diese gemeinsame Konfiguration entstehen leicht zwei unabhängig gepflegte Markup-Strukturen, die bei jeder Änderung doppelt angepasst werden müssen und dabei erfahrungsgemäß schnell auseinanderlaufen.
8. Responsive Komponenten testen ohne echten Browser-Resize
Automatisierte Tests für responsive Vue Komponenten scheitern oft daran, dass Testumgebungen wie Vitest mit Happy DOM oder JSDOM keinen echten Viewport haben, dessen Breite sich verändern lässt. Wer Breakpoint-Logik direkt an window.innerWidth koppelt, kann sie in Komponententests kaum zuverlässig auslösen. Deshalb lohnt es sich, das Breakpoint-Composable so zu bauen, dass es in Tests durch einen Mock ersetzt werden kann, statt echte Browser-Events zu benötigen.
Weil useAppBreakpoints aus Abschnitt zwei ein reines Composable mit reaktiven Rückgabewerten ist, lässt es sich in Tests einfach mocken, ohne echtes Resize-Verhalten zu simulieren. Für Container-Query-basierte Komponenten ist ein zusätzlicher visueller Regressionstest mit Playwright sinnvoll, weil sich reines CSS-Verhalten in Komponententests kaum zuverlässig prüfen lässt.
// AdaptiveTable.spec.js — mocking the breakpoint composable in Vitest
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import AdaptiveTable from '@/components/AdaptiveTable.vue'
vi.mock('@/composables/useAppBreakpoints', () => ({
useAppBreakpoints: () => ({ isMobile: { value: true } }),
}))
describe('AdaptiveTable', () => {
it('renders card layout when isMobile is true', () => {
const wrapper = mount(AdaptiveTable, { props: { rows: [{ name: 'Test' }] } })
expect(wrapper.find('[data-testid="mobile-card"]').exists()).toBe(true)
expect(wrapper.find('table').exists()).toBe(false)
})
})
9. Ansätze im Vergleich: Media Query, Container Query, JS-Breakpoint
Für responsive Vue Komponenten existieren drei grundlegende Techniken, die sich in ihrem Anwendungsbereich klar unterscheiden und sich in der Praxis eher ergänzen als ausschließen.
| Technik | Reagiert auf | Am besten geeignet für |
|---|---|---|
| CSS Media Query | Viewport-Breite | Globale Seiten-Layouts, Sidebar ein/aus |
| Container Query | Breite des Elternelements | Wiederverwendbare Komponenten in wechselnden Kontexten |
| JS-Breakpoint-Composable | Viewport-Breite, reaktiv in JS | Strukturelle Unterschiede, andere Komponenten laden |
In der Praxis kombiniert man alle drei Techniken je nach Problem: CSS Media Queries für das grobe Seiten-Layout, Container Queries für einzelne wiederverwendbare Komponenten und JS-Breakpoint-Composables dort, wo sich nicht nur Styling, sondern tatsächliche Komponentenstruktur oder Datenlade-Verhalten ändern muss. Wer versucht, alle drei Fälle mit nur einer Technik zu lösen, landet entweder bei unnötig komplexem CSS oder bei JavaScript-Logik für simple visuelle Anpassungen, die CSS allein besser lösen würde.
Mironsoft
Vue-Entwicklung, responsive Design-Systeme und Component-Architektur
Komponenten, die überall funktionieren, egal wo sie landen?
Wir bauen wiederverwendbare Vue-Komponenten mit Container Queries, Breakpoint-Composables und sauberer Teststrategie, damit responsive Verhalten nicht bei jeder neuen Einbindung neu erfunden werden muss.
Component-Audit
Bestehende Komponenten auf Media-Query-Anti-Patterns prüfen
Container Queries
Layout-Logik von der Viewport-Breite entkoppeln
Testbare Composables
Breakpoint-Logik mockbar und wartbar strukturieren
10. Zusammenfassung
Responsive Vue Komponenten brauchen mehr als reine CSS-Media-Queries, sobald sie in unterschiedlichen Layout-Kontexten wiederverwendet werden. Container Queries lösen das Kernproblem, indem Komponenten auf die Breite ihres eigenen Containers statt auf die Viewport-Breite reagieren. Ein zentrales Breakpoint-Composable liefert reaktive JavaScript-Werte für strukturelle Entscheidungen, die über Styling hinausgehen, etwa komplett unterschiedliche Navigationskomponenten für Mobile und Desktop.
Adaptive Slots geben Aufrufern die Kontrolle über konkrete Inhalte je Breakpoint, während Conditional Rendering mit v-if verhindert, dass ungenutzte Komponentenvarianten unnötig Ressourcen verbrauchen. Gemeinsame Datenstrukturen wie Spalten-Konfigurationen verhindern doppelte Pflege zwischen Tabellen- und Karten-Darstellung. Wer diese Patterns kombiniert einsetzt, bekommt responsive Vue Komponenten, die tatsächlich überall funktionieren, egal in welchem Kontext sie eingebunden werden.
Responsive Vue Komponenten Patterns — Das Wichtigste auf einen Blick
Container Queries
Reagieren auf die Breite des Elternelements, nicht des Viewports. Ideal für wiederverwendbare Komponenten.
Breakpoint-Composable
Zentrale, reaktive Wahrheitsquelle für strukturelle Entscheidungen jenseits von reinem CSS.
Conditional Rendering
v-if/v-else statt CSS-Verstecken spart Lifecycle-Overhead und doppelte Listener.
Gemeinsame Datenquellen
Spalten- und Navigationskonfiguration einmal definieren, in mehreren Layout-Varianten wiederverwenden.