Responsive Komponenten ohne ResizeObserver-Code
Viewport-Breakpoints reichen nicht – Komponenten müssen auf ihre eigene Größe reagieren, nicht auf die Fensterbreite. x-resize bringt den ResizeObserver deklarativ in Alpine.js, ohne eine Zeile Boilerplate-Code.
Inhaltsverzeichnis
- 1. Das Problem mit window.resize und Viewport-Breakpoints
- 2. Der native ResizeObserver und sein Boilerplate
- 3. Das x-resize Plugin: Installation und Grundprinzip
- 4. x-resize Grundlagen: width und height lesen
- 5. Komponentenbreakpoints ohne CSS-Container-Queries
- 6. Responsive Tabellen mit x-resize
- 7. Sidebar-Layout: Breakpoint auf Komponentenebene
- 8. Performance: ResizeObserver richtig nutzen
- 9. x-resize vs. CSS Container Queries vs. window.resize
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem mit window.resize und Viewport-Breakpoints
Responsive Design wird traditionell über Viewport-Breakpoints implementiert: Wenn das Browserfenster schmaler als 768 Pixel ist, wechselt das Layout. Das funktioniert gut für Seitenstruktur und globale Layouts, versagt aber bei wiederverwendbaren Komponenten. Eine Produktkarte, die in der Hauptspalte vier Produkte nebeneinander zeigt, muss in einer schmaleren Sidebar zwei zeigen – obwohl der Viewport identisch ist. Der Viewport sagt der Komponente nichts über ihre eigene verfügbare Breite.
Das klassische Workaround war window.addEventListener('resize', handler). Dieser Ansatz hat mehrere Probleme: Er reagiert auf Fenstergrößenänderungen, nicht auf Elementgrößenänderungen. Eine Komponente, die durch Sidebar-Toggle, Accordion oder dynamisches Layout größer oder kleiner wird, löst keinen resize-Event aus. Der Handler muss selbst die Breite des Elements via getBoundingClientRect() abfragen, was einen Layout-Reflow erzwingt. Das Ergebnis ist fragiler, schwer testbarer Code, der Sonderfälle für jeden möglichen Layout-Kontext erfordert.
Der native ResizeObserver löst genau dieses Problem – er beobachtet Größenänderungen einzelner Elemente, unabhängig von der Ursache. Aber die direkte Nutzung erfordert Boilerplate: Observer instanziieren, registrieren, bei Cleanup beobachten und beim Zerstören der Komponente trennen. x-resize kapselt diese Logik vollständig in eine einzige Alpine.js-Direktive.
2. Der native ResizeObserver und sein Boilerplate
Zum Verständnis, was x-resize unter der Haube tut, lohnt ein Blick auf den nativen ResizeObserver. Die Browser-API ist konzeptionell einfach: Man erstellt einen Observer mit einem Callback, ruft observer.observe(element) auf und der Callback wird immer dann aufgerufen, wenn sich die Größe des Elements ändert. Im Callback erhält man ein Array von ResizeObserverEntry-Objekten mit contentRect, das die neue Größe enthält.
Der Boilerplate-Aufwand in Alpine.js ohne x-resize ist erheblich: In x-init Observer erstellen und starten, in x-effect oder über $refs auf Daten reagieren, und im Cleanup-Hook ($destroy oder Lifecycle) den Observer wieder trennen. Vergisst man das Trennen, entsteht ein Memory-Leak, weil der Observer die Referenz auf das Element hält und es nicht vom Garbage Collector freigegeben werden kann. x-resize übernimmt diesen Lifecycle vollständig automatisch.
// WITHOUT x-resize: manual ResizeObserver boilerplate in Alpine.js
<div x-data="{
width: 0,
height: 0,
observer: null,
init() {
this.observer = new ResizeObserver(([entry]) => {
this.width = Math.round(entry.contentRect.width)
this.height = Math.round(entry.contentRect.height)
})
this.observer.observe(this.$el)
},
destroy() {
// Must disconnect manually — or you get a memory leak!
if (this.observer) this.observer.disconnect()
}
}">
<p x-text="`Width: ${width}px, Height: ${height}px`"></p>
</div>
// WITH x-resize: zero boilerplate, lifecycle handled automatically
<div
x-data="{ width: 0, height: 0 }"
x-resize="width = $width; height = $height"
>
<p x-text="`Width: ${width}px, Height: ${height}px`"></p>
</div>
3. Das x-resize Plugin: Installation und Grundprinzip
Das x-resize Plugin ist wie x-mask ein offizielles Alpine.js Plugin und wird auf dieselbe Weise installiert. Es muss vor Alpine.start() mit Alpine.plugin(resize) registriert werden. In Hyvä-Projekten empfiehlt sich die npm-Installation und Einbindung im zentralen Tailwind-Build-Entrypoint. Das Plugin ist minimal und fügt keine signifikante Bundle-Größe hinzu.
Das Grundprinzip von x-resize ist einfach: Die Direktive wird auf ein Element gesetzt und bekommt einen JavaScript-Ausdruck als Wert. Dieser Ausdruck wird jedes Mal ausgeführt, wenn sich die Größe des Elements ändert. Im Ausdruck stehen zwei magische Variablen zur Verfügung: $width und $height, die die aktuelle Breite und Höhe des Elements in Pixeln enthalten. Diese Werte können direkt in Alpine-Datenproperties geschrieben oder für berechnete Breakpoints genutzt werden.
4. x-resize Grundlagen: width und height lesen
Der einfachste Einsatz von x-resize ist das Beobachten der aktuellen Elementgröße und das Speichern in Alpine-Daten. x-resize="width = $width; height = $height" auf einem Container-Element sorgt dafür, dass width und height in den Alpine-Daten immer den aktuellen Werten entsprechen. Diese Werte sind reaktiv: Jedes x-text, x-show oder x-bind das darauf basiert, aktualisiert sich automatisch.
Ein wichtiger Aspekt: x-resize wird einmal initial aufgerufen, wenn die Komponente initialisiert wird. Das bedeutet, die Größe ist von Anfang an bekannt – nicht erst nach der ersten manuellen Größenänderung. Für Komponenten, die ihr Layout sofort korrekt darstellen müssen (nicht erst nach dem ersten Window-Resize), ist das entscheidend. Der Callback wird außerdem debounced, um bei schnellen, kontinuierlichen Größenänderungen (z.B. beim Ziehen der Fensterkante) nicht in jedem Frame aufgerufen zu werden.
// Basic x-resize usage: track element size reactively
<div
x-data="{
containerWidth: 0,
get breakpoint() {
if (this.containerWidth < 400) return 'xs'
if (this.containerWidth < 640) return 'sm'
if (this.containerWidth < 768) return 'md'
return 'lg'
},
get columns() {
return { xs: 1, sm: 2, md: 3, lg: 4 }[this.breakpoint]
}
}"
x-resize="containerWidth = $width"
class="w-full"
>
<!-- Layout adapts to container width, not viewport width -->
<p class="text-xs text-slate-500 mb-4">
Container: <span x-text="containerWidth + 'px'"></span>
· Breakpoint: <span x-text="breakpoint"></span>
· Spalten: <span x-text="columns"></span>
</p>
<div :class="`grid gap-4 grid-cols-${columns}`">
<template x-for="i in 8" :key="i">
<div class="bg-teal-100 rounded p-4 text-center text-sm font-medium"
x-text="`Produkt ${i}`"></div>
</template>
</div>
</div>
5. Komponentenbreakpoints ohne CSS-Container-Queries
CSS Container Queries sind ein modernes Browser-Feature, das Breakpoints auf Containerebene ermöglicht – ähnlich dem, was x-resize für JavaScript-Logik tut. Beide Ansätze sind komplementär, nicht konkurrierend. CSS Container Queries steuern reine CSS-Anpassungen wie Schriftgrößen, Abstände und Farben, während x-resize JavaScript-Logik und DOM-Struktur steuert: welche Komponente angezeigt wird, wie viele Spalten ein Grid hat, ob ein Accordion oder Tabs genutzt wird.
Für Teams, die noch auf ältere Browser abzielen müssen oder komplexere JavaScript-Logik basierend auf der Containergröße benötigen, ist x-resize die zuverlässigere Lösung. Die Breakpoints werden als berechnete Getter in Alpine-Daten definiert und sind damit vollständig testbar und inspizierbar. Ein Getter get isWide() { return this.containerWidth >= 640 } ist klarer als ein CSS-Selector und kann in komplexeren Entscheidungsbäumen wiederverwendet werden.
6. Responsive Tabellen mit x-resize
Tabellen sind eines der schwierigsten Elemente im Responsive Design. Eine Datentabelle mit acht Spalten lässt sich nicht auf 320 Pixeln korrekt darstellen – horizontales Scrollen ist unbefriedigend, Textkürzungen sind unleserlich. Das Standardmuster in modernen Webanwendungen ist die Umwandlung in eine Card-Liste auf kleinen Bildschirmen: Jede Zeile wird zu einer Karte, bei der Spaltenkopf und Wert untereinander stehen.
Mit x-resize lässt sich dieser Wechsel direkt auf Containerebene steuern. Wenn die Breite des Tabellen-Containers unter einen Schwellenwert fällt, wechselt die Alpine-Komponente von Tabellen- auf Card-Layout. Das ist präziser als ein Viewport-Breakpoint, weil die Tabelle korrekt reagiert, egal ob sie in einer breiten Hauptspalte oder einer schmalen Sidebar gerendert wird. Das Template enthält beide Layouts, x-show steuert welches aktiv ist.
<!-- Responsive table: switches to card layout when container is narrow -->
<div
x-data="{
containerWidth: 0,
get useCardLayout() { return this.containerWidth < 560 },
orders: [
{ id: '2026-001', date: '10.05.2026', product: 'Alpine.js Kurs', amount: '49,00 €', status: 'Bezahlt' },
{ id: '2026-002', date: '09.05.2026', product: 'Hyvä Lizenz', amount: '199,00 €', status: 'Offen' },
{ id: '2026-003', date: '08.05.2026', product: 'DevOps Paket', amount: '349,00 €', status: 'Bezahlt' }
]
}"
x-resize="containerWidth = $width"
>
<!-- Table layout (wide containers) -->
<table x-show="!useCardLayout" class="w-full text-sm border-collapse">
<thead class="bg-slate-100">
<tr>
<th class="text-left p-3 font-semibold">Bestell-Nr.</th>
<th class="text-left p-3 font-semibold">Datum</th>
<th class="text-left p-3 font-semibold">Produkt</th>
<th class="text-left p-3 font-semibold">Betrag</th>
<th class="text-left p-3 font-semibold">Status</th>
</tr>
</thead>
<tbody>
<template x-for="o in orders" :key="o.id">
<tr class="border-t border-slate-200">
<td class="p-3" x-text="o.id"></td>
<td class="p-3" x-text="o.date"></td>
<td class="p-3" x-text="o.product"></td>
<td class="p-3 font-semibold" x-text="o.amount"></td>
<td class="p-3"><span class="px-2 py-1 rounded text-xs font-bold bg-teal-100 text-teal-700" x-text="o.status"></span></td>
</tr>
</template>
</tbody>
</table>
<!-- Card layout (narrow containers) -->
<div x-show="useCardLayout" class="space-y-3">
<template x-for="o in orders" :key="o.id">
<div class="border border-slate-200 rounded-xl p-4 text-sm">
<div class="flex justify-between mb-2">
<span class="font-semibold" x-text="o.id"></span>
<span class="px-2 py-1 rounded text-xs font-bold bg-teal-100 text-teal-700" x-text="o.status"></span>
</div>
<p class="text-slate-600" x-text="o.product"></p>
<div class="flex justify-between mt-2 text-xs text-slate-500">
<span x-text="o.date"></span>
<span class="font-semibold text-slate-800" x-text="o.amount"></span>
</div>
</div>
</template>
</div>
</div>
7. Sidebar-Layout: Breakpoint auf Komponentenebene
Ein typisches Hyvä-Layout enthält einen Hauptbereich und eine optionale Sidebar. Wenn die Sidebar eingeblendet ist, wird der Hauptbereich schmaler – eine Produktliste, die vorher vier Spalten hatte, muss auf drei oder zwei Spalten wechseln. Ein Viewport-Breakpoint weiß nicht, ob die Sidebar offen oder geschlossen ist. x-resize weiß es, weil es die aktuelle Breite des Containers in Echtzeit beobachtet.
Das Muster ist dabei simpel: Die Produktliste-Komponente hat x-resize, das die Container-Breite trackt. Ein Getter berechnet die Spaltenanzahl. Wenn der Benutzer die Sidebar per Button ein- oder ausblendet, ändert sich die Breite des Produktlisten-Containers, x-resize feuert, die Spaltenanzahl wird neu berechnet, und das Grid passt sich automatisch an – ohne dass die Produktliste wissen muss, dass eine Sidebar existiert. Das ist echte Komponentenkapselung.
8. Performance: ResizeObserver richtig nutzen
ResizeObserver-Callbacks werden synchron vor dem nächsten Paint des Browsers aufgerufen. Das ist mächtig, kann aber zu Performance-Problemen führen, wenn der Callback selbst DOM-Änderungen verursacht, die weitere Größenänderungen auslösen – ein sogenannter Resize-Loop. Alpine.js und x-resize vermeiden das nicht automatisch. Entwickler müssen darauf achten, dass im x-resize-Ausdruck keine direkten DOM-Manipulationen passieren, die die beobachtete Elementgröße verändern.
In der Praxis ist der häufigste Fall unproblematisch: x-resize schreibt nur in Alpine-Datenproperties, und Alpine aktualisiert den DOM asynchron via seinen Reaktivitätsmechanismus. Das führt nicht zu synchronen Resize-Loops. Problematisch würde es, wenn man im x-resize-Ausdruck direkt $el.style.height setzt, was sofort ein weiteres Resize-Event auslösen kann. Der richtige Weg: Größe in Daten schreiben, Layout per :class oder :style-Binding steuern.
9. x-resize vs. CSS Container Queries vs. window.resize
Alle drei Ansätze lösen das Problem der komponentenabhängigen Responsivität, aber auf verschiedenen Ebenen und mit verschiedenen Stärken. CSS Container Queries sind eine reine CSS-Lösung, brauchen kein JavaScript und sind performant – sie eignen sich für alle Layoutanpassungen, die sich in CSS ausdrücken lassen. x-resize ist die JavaScript-Erweiterung: Es steuert logische Entscheidungen, DOM-Struktur und Alpine-Zustände. window.resize ist der Ansatz der Vergangenheit, der keinen Platz mehr in modernen Alpine.js-Projekten hat.
| Ansatz | Reaktion auf | JavaScript nötig? | Stärke |
|---|---|---|---|
| window.resize | Fensterbreite | Ja (viel Boilerplate) | Veraltet, ungenau |
| CSS Container Queries | Container-Breite | Nein | Reine CSS-Lösung, performant |
| x-resize (Alpine) | Element-Größe | Alpine.js (deklarativ) | JS-Logik, DOM-Struktur, kein Boilerplate |
| Nativer ResizeObserver | Element-Größe | Ja (viel Boilerplate) | Maximum Kontrolle, aufwändig |
Die Empfehlung für Hyvä-Projekte ist ein kombinierter Ansatz: CSS Container Queries für Layout-Anpassungen, die sich rein in CSS ausdrücken lassen, und x-resize für alles, was Alpine.js-Zustand oder DOM-Struktur betrifft. Beide können auf demselben Element aktiv sein – sie interferieren nicht miteinander. window.resize hat in neuen Komponenten keine Daseinsberechtigung mehr.
Mironsoft
Alpine.js · Hyvä Themes · Magento 2 Frontend-Entwicklung
Komponenten die in jedem Layout funktionieren?
Wir bauen Hyvä-Komponenten, die auf ihre eigene Breite reagieren – für Produktgalerien, Datentabellen und Filterleisten die in jeder Sidebar und jedem Hauptbereich korrekt dargestellt werden.
Container-Awareness
Komponenten die auf ihre eigene Breite reagieren – nicht auf den Viewport
Responsive Tabellen
Automatischer Wechsel zwischen Tabellenansicht und Card-Layout je nach verfügbarer Breite
Layout-Refactoring
window.resize-Listener durch x-resize ersetzen – wartbarer, korrekter, ohne Boilerplate
10. Zusammenfassung
Alpine.js x-resize ist die deklarative Antwort auf eine der schwierigsten Herausforderungen im Responsive Design: Komponenten, die auf ihre eigene Größe reagieren müssen, nicht auf den Viewport. Das Plugin kapselt den nativen ResizeObserver vollständig – inklusive Lifecycle-Management, initiaem Aufruf und Disconnect beim Destroy. Das Ergebnis sind Komponenten, die in jeder Sidebar-Konfiguration, jedem Grid-Kontext und jedem Layout-Wechsel korrekt reagieren, ohne window.resize-Handler oder manuelle Observer-Verwaltung.
Der wichtigste Unterschied zu CSS Container Queries: x-resize steuert JavaScript-Logik und DOM-Struktur, keine CSS-Properties. Für Breakpoints, die nur visuelle Anpassungen betreffen (Schriftgröße, Abstände, Farben), sind Container Queries die richtige Wahl. Für Breakpoints, die Alpine-Zustände ändern, DOM-Elemente ein- und ausblenden oder Spaltenanzahlen berechnen, ist x-resize unverzichtbar. In modernen Hyvä-Projekten arbeiten beide zusammen.
Alpine.js x-resize — Das Wichtigste auf einen Blick
Magische Variablen
$width und $height enthalten die aktuelle Elementgröße in Pixeln. Verfügbar im x-resize-Ausdruck, der bei jeder Größenänderung ausgeführt wird.
Lifecycle automatisch
x-resize startet beim Initialisieren, feuert initial einmal, und trennt den Observer automatisch beim Destroy der Komponente. Kein manuelles Cleanup nötig.
Keine Resize-Loops
Nur Alpine-Daten im x-resize-Ausdruck verändern. Keine direkten DOM-Manipulationen, die die Elementgröße verändern – sonst entsteht ein Resize-Loop.
Kombination mit CSS
CSS Container Queries für visuelle Anpassungen, x-resize für JavaScript-Logik. Beide können auf demselben Element aktiv sein ohne Interferenz.