x-data
Alpine
Alpine.js · Rating Widget · Kein jQuery · Frontend
Alpine.js Rating-Widget:
Sterne-Bewertung ohne jQuery

jQuery für ein simples Sterne-Widget zu laden ist wie einen LKW zu mieten, um einen Brief zu transportieren. Alpine.js bietet mit x-data, x-on und x-bind alles, was ein vollständiges, barrierefreies Rating-Widget braucht – in unter 50 Zeilen, ohne npm, ohne Build-Step, direkt im Template.

8 Min. Lesezeit x-data · x-on · x-bind · hover-State · Accessibility Alpine.js 3.x · Hyvä Themes · Magento 2

1. Warum Alpine.js statt jQuery für ein Rating-Widget?

In vielen Magento-Projekten findet man noch immer jQuery-Plugins wie jQuery Star Rating oder Raty.js, die einen vollständigen DOM-Manipulations-Layer mitbringen, nur damit fünf Sterne auf Klick und Hover reagieren. Das Resultat sind zusätzliche HTTP-Requests, globale Namespace-Verschmutzung und Markup, das im DOM nachträglich durch JavaScript erzeugt wird – was serverseitiges Rendering und SEO erschwert. Alpine.js löst dasselbe Problem deklarativ: Der State lebt direkt im HTML-Attribut, kein externes Paket wird geladen, der DOM bleibt serverseitig vollständig lesbar.

Der entscheidende Vorteil von Alpine.js für diesen Anwendungsfall ist die Direktive x-data, die einen lokal abgegrenzten reaktiven Daten-Scope erzeugt. Alle Daten des Widgets – aktuelle Bewertung, Hover-Vorschau, Lade-State – leben in diesem Objekt. Keine globalen Variablen, keine Event-Delegation über den gesamten Document-Body, kein Konfliktpotenzial mit anderen Widgets auf derselben Seite. Jedes Rating-Widget auf der Seite ist vollständig isoliert, obwohl sie dasselbe Template verwenden.

Für Hyvä Themes in Magento 2 ist Alpine.js die native Wahl: Hyvä lädt Alpine.js als Standard-JavaScript-Layer und schließt jQuery aus dem Frontend-Stack aus. Wer für ein Rating-Widget jQuery nachladen würde, würde damit bewusst die Performance-Strategie von Hyvä unterlaufen. Die alpine-native Implementierung passt sich nahtlos in das bestehende Event-System von Hyvä ein und profitiert von der CSP-konformen Inline-Script-Registrierung.

2. Die Grundstruktur: x-data und reaktiver State

Das Rating-Widget beginnt mit einem x-data-Objekt, das alle benötigten State-Variablen enthält: rating für die gespeicherte Bewertung (initial 0), hoverRating für die Vorschau beim Darüberfahren (initial 0), submitted als Boolean-Flag nach dem Absenden und loading für den API-Call-State. Dieses Objekt ist der einzige State-Container – Alpine.js stellt automatisch sicher, dass sich das DOM bei jeder Änderung dieser Werte aktualisiert, ohne dass manuelles DOM-Traversal nötig ist.

Die Sterne selbst werden über eine x-for-Schleife über ein Array [1, 2, 3, 4, 5] gerendert. Jeder Stern ist ein <button>-Element mit x-on:click, x-on:mouseenter und x-on:mouseleave. Die aktive Klasse – ob der Stern ausgefüllt oder leer dargestellt wird – wird über x-bind:class berechnet: ein Stern ist aktiv, wenn sein Index kleiner oder gleich dem aktuell angezeigten Wert ist, also i <= (hoverRating || rating). Diese einzige Formel steuert sowohl den Hover-Preview als auch die persistierte Bewertung.


// Alpine.js Rating Widget — minimal state, full reactivity
function ratingWidget() {
  return {
    rating: 0,        // persisted selection
    hoverRating: 0,   // preview on hover
    submitted: false,
    loading: false,
    stars: [1, 2, 3, 4, 5],

    // Returns true if star i should render as filled
    isActive(i) {
      return i <= (this.hoverRating || this.rating);
    },

    setHover(i) { this.hoverRating = i; },
    clearHover()  { this.hoverRating = 0; },

    select(i) {
      this.rating = i;
      this.$dispatch('rating-selected', { value: i });
    }
  };
}

3. Hover-State: Sterne-Preview beim Darüberfahren

Der Hover-State ist das visuell auffälligste Element eines Rating-Widgets und technisch der Punkt, an dem naive Implementierungen fragil werden. Das übliche jQuery-Muster manipuliert Klassen auf allen Geschwister-Elementen – bei fünf Sternen sind das fünf separate DOM-Operationen pro Maus-Event. Mit Alpine.js ist das Prinzip umgekehrt: statt des DOMs wird nur eine einzige Variable hoverRating verändert. Alpine berechnet dann für jedes Stern-Element eigenständig, ob es die aktive Klasse erhält – deklarativ statt imperativ.

Die Direktive x-on:mouseenter="setHover(i)" setzt hoverRating auf den Wert des aktuellen Sterns. x-on:mouseleave="clearHover()" setzt ihn zurück auf 0. Die visuelle Klasse wird mit :class="{ 'text-yellow-400': isActive(i), 'text-slate-300': !isActive(i) }" gebunden. Das Ergebnis: wenn der Nutzer über Stern 3 fährt, zeigt das Widget sofort drei gelbe und zwei graue Sterne – ohne DOM-Traversal, ohne querySelectorAll, ohne manuelle Schleife.

Ein wichtiges Detail für die korrekte UX: mouseleave muss auf dem Container des gesamten Widgets registriert werden, nicht auf jedem einzelnen Stern. Andernfalls flackert der Hover-State, wenn der Cursor zwischen zwei Sternen liegt und kurz keinen davon berührt. Mit x-on:mouseleave.self="clearHover()" am Container-Element wird clearHover() nur ausgelöst, wenn der Mauszeiger den Container-Bereich verlässt – nicht beim Wechsel zwischen Stern-Buttons.

4. Klick-Persistenz: Bewertung speichern und anzeigen

Eine Bewertung zu speichern bedeutet in Alpine.js: die Variable rating zu setzen und optional in localStorage zu persistieren. Die select(i)-Methode setzt this.rating = i und löst ein Custom Event aus. Für die optionale Persistenz im localStorage – nützlich, wenn eine Bewertung pro Session nur einmal abgegeben werden soll – reicht ein einzeiliger localStorage.setItem-Aufruf im selben Handler. Beim Initialisieren des Widgets liest init() den gespeicherten Wert aus und setzt this.rating entsprechend.

Nach dem Absenden wechselt das Widget in einen Read-only-Mode: die Buttons werden mit :disabled="submitted" deaktiviert, der Hover-State greift nicht mehr, und eine Bestätigungsnachricht wird mit x-show="submitted" eingeblendet. Dieser State-Übergang ist in Alpine.js vollständig deklarativ: man ändert submitted auf true, und sämtliche abhängigen Direktiven – x-show, :disabled, :class – aktualisieren sich automatisch. Kein manuelles Durchlaufen des DOM.


// Extended rating widget with localStorage persistence
function ratingWidget(productId) {
  return {
    rating: 0,
    hoverRating: 0,
    submitted: false,
    loading: false,
    stars: [1, 2, 3, 4, 5],
    storageKey: `rating_${productId}`,

    init() {
      const saved = localStorage.getItem(this.storageKey);
      if (saved) {
        this.rating = parseInt(saved, 10);
        this.submitted = true; // already rated this session
      }
    },

    isActive(i) { return i <= (this.hoverRating || this.rating); },
    setHover(i) { if (!this.submitted) this.hoverRating = i; },
    clearHover() { this.hoverRating = 0; },

    select(i) {
      if (this.submitted) return;
      this.rating = i;
      localStorage.setItem(this.storageKey, i);
    }
  };
}

5. Halbe Sterne mit SVG-Clip und Alpine-Logik

Halbe Sterne sind optisch ansprechend für die Anzeige eines Durchschnittswerts (z.B. 3,7 von 5) und erfordern eine leicht erweiterte Alpine-Logik. Die Implementierung nutzt SVG-Sterne mit einem <clipPath>-Element: Jeder Stern besteht aus zwei überlagerten SVG-Pfaden – einem grauen Hintergrund und einem gelben Vordergrund. Der Vordergrund wird über einen dynamischen clip-path zu 50% oder 100% eingeblendet, abhängig davon, ob der angezeigte Wert eine halbe oder volle Bewertung für diesen Stern enthält.

Die Alpine-Hilfsfunktion starFill(i, value) gibt 'full', 'half' oder 'empty' zurück: Wenn value >= i ist der Stern voll, wenn value >= i - 0.5 ist er halb, sonst leer. Im Template wird dann mit :style="{ clipPath: starFill(i, displayValue) === 'half' ? 'inset(0 50% 0 0)' : 'none' }" gearbeitet. Halbe Sterne werden typischerweise nur für die Anzeige des Durchschnittswerts verwendet – die Eingabe bleibt bei ganzen Zahlen, da Nutzer keine halben Sterne als Bewertung eingeben sollen.

6. Barrierefreiheit: ARIA-Attribute und Tastatursteuerung

Ein barrierefreies Rating-Widget braucht ARIA-Attribute, die Screenreadern den aktuellen State kommunizieren. Jeder Stern-Button erhält :aria-label="`${i} von 5 Sternen`" und :aria-pressed="rating === i". Der Container bekommt role="radiogroup" und aria-label="Produktbewertung". Mit diesen Attributen gibt ein Screenreader beim Navigieren zwischen den Buttons an, wie viele Sterne aktiv sind – ohne visuell sichtbaren Text, der das Design stören würde.

Tastatursteuerung ist bei nativen <button>-Elementen bereits eingebaut: Tab-Navigation, Enter und Space lösen click aus. Für Pfeil-Navigation innerhalb der Sterngruppe – wie es ARIA für Radiogroups vorsieht – fügt man x-on:keydown.arrow-right.prevent="select(Math.min(rating + 1, 5))" und x-on:keydown.arrow-left.prevent="select(Math.max(rating - 1, 1))" am Container hinzu. Alpine.js unterstützt Keyboard-Modifikatoren nativ, sodass keine manuelle Event-Listener-Registrierung nötig ist.


<!-- Accessible Alpine.js rating widget markup -->
<div
  x-data="ratingWidget('prod-42')"
  role="radiogroup"
  aria-label="Produktbewertung"
  x-on:mouseleave.self="clearHover()"
  x-on:keydown.arrow-right.prevent="select(Math.min(rating + 1, 5))"
  x-on:keydown.arrow-left.prevent="select(Math.max(rating - 1, 1))"
  class="flex items-center gap-1"
>
  <template x-for="i in stars" :key="i">
    <button
      type="button"
      x-on:click="select(i)"
      x-on:mouseenter="setHover(i)"
      :aria-label="`${i} von 5 Sternen`"
      :aria-pressed="rating === i"
      :disabled="submitted"
      :class="isActive(i) ? 'text-yellow-400' : 'text-slate-300'"
      class="text-2xl transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-500 rounded"
    >★</button>
  </template>
  <span x-show="submitted" x-cloak class="ml-3 text-sm text-teal-700 font-semibold">Vielen Dank!</span>
</div>

7. Integration in Magento 2 und Hyvä Themes

In Hyvä Themes wird das Rating-Widget als .phtml-Template eingebunden. Das Alpine-Skript wird als Inline-Script am Ende des Templates platziert und mit $hyvaCsp->registerInlineScript() für die Content Security Policy registriert. Das Widget selbst lebt im Template-Markup und nutzt das von Hyvä bereitgestellte Alpine.js – kein zusätzliches <script src> nötig. Die Produkt-ID wird als PHP-Variable in das x-data-Attribut interpoliert: x-data="ratingWidget('<?= $escaper->escapeHtmlAttr($productId) ?>')".

Für die Darstellung vorhandener Bewertungen aus Magento-Daten übergibt man den Durchschnittswert als PHP-Variable und rendert das Read-only-Widget mit einem separaten Template-Block. Der Write-Mode (Eingabe) ist nur für eingeloggte Kunden sichtbar, gesteuert über $customerSession->isLoggedIn() im Block-ViewModel. Diese Trennung von Lese- und Schreibmodus hält das Template übersichtlich und ermöglicht unterschiedliche Caching-Strategien für beide Varianten.

8. Bewertung per Fetch an eine API senden

Wenn der Nutzer einen Stern auswählt und auf "Bewertung abgeben" klickt, sendet die submit()-Methode die Daten per fetch an eine REST-Endpoint. Während des API-Calls ist loading auf true gesetzt – das Template blendet einen Spinner ein und deaktiviert den Submit-Button, um Doppel-Submissions zu verhindern. Bei Erfolg wird submitted = true gesetzt, was das gesamte Widget in den Read-only-State überführt. Bei einem Fehler wird eine Fehlermeldung in errorMessage geschrieben, die mit x-show="errorMessage" angezeigt wird.

Alpine.js bietet mit $fetch keinen eingebauten HTTP-Client – fetch aus der Browser-API wird direkt genutzt. Das ist kein Nachteil: Die native fetch-API ist in allen modernen Browsern verfügbar, benötigt kein zusätzliches Polyfill und gibt Promises zurück, die in async/await-Methoden von Alpine-Komponenten problemlos genutzt werden. Der Magento REST-Endpoint für Produktbewertungen lautet /rest/V1/reviews und erwartet ein JSON-Objekt mit productId, rating und optional nickname.


// Submit method with fetch, loading state and error handling
async submit() {
  if (!this.rating || this.submitted || this.loading) return;
  this.loading = true;
  this.errorMessage = '';

  try {
    const response = await fetch('/rest/V1/reviews', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'
      },
      body: JSON.stringify({
        productId: this.productId,
        rating: this.rating,
        nickname: this.nickname || 'Anonym'
      })
    });

    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    this.submitted = true;
    localStorage.setItem(this.storageKey, this.rating);
    this.$dispatch('rating-submitted', { productId: this.productId, rating: this.rating });

  } catch (err) {
    this.errorMessage = 'Bewertung konnte nicht gespeichert werden. Bitte versuche es erneut.';
    console.error('[RatingWidget]', err);
  } finally {
    this.loading = false;
  }
}

9. Alpine.js vs. jQuery Rating: direkter Vergleich

Der Vergleich zwischen einer jQuery-basierten und einer Alpine.js-basierten Rating-Implementierung zeigt deutliche Unterschiede in Codemenge, DOM-Abhängigkeit und Wartbarkeit. Beide Ansätze erzielen dasselbe visuelle Ergebnis, unterscheiden sich aber fundamental in ihrer Architektur.

Kriterium jQuery Rating (Raty.js) Alpine.js Rating Ergebnis
Abhängigkeiten jQuery + Plugin (~90 KB gzip) Alpine.js (~15 KB gzip) Alpine 6× kleiner
DOM-Generierung JS erzeugt Markup zur Laufzeit Markup serverseitig vollständig Alpine SEO-freundlicher
Mehrere Widgets Globale Selektoren, Konflikte möglich Isolierter x-data-Scope Alpine konfliktfrei
Accessibility Manuell nachzurüsten ARIA nativ in Direktiven Alpine wartbarer
Hyvä-Kompatibilität Erfordert jQuery-Nachladen Nativ, kein Extra-Script Alpine empfohlen

Besonders relevant ist der Unterschied bei der DOM-Generierung. jQuery-basierte Rating-Plugins ersetzen ein einfaches Input-Element durch generierten DOM-Code – Sterne, Bilder oder SVGs werden zur Laufzeit eingefügt. Das macht serverseitiges Rendering und Crawling schwieriger und verhindert, dass CSS korrekt auf den anfänglichen Render-State angewendet wird. Alpine.js rendert dagegen das vollständige Markup im Template und aktiviert nur die reaktiven Bindings – der initiale Render-State ist immer korrekt, auch ohne JavaScript.

Mironsoft

Alpine.js Frontend-Entwicklung für Hyvä Themes und Magento 2

Alpine.js-Widgets für euren Magento-Shop?

Wir entwickeln performante, barrierefreie Alpine.js-Komponenten für Hyvä Themes – Rating-Widgets, Produktkonfiguratoren, Filter und mehr. Kein jQuery, kein Build-Step-Overhead, vollständig CSP-konform.

Widget-Entwicklung

Rating, Galerie, Konfigurator – Alpine.js-Komponenten ohne jQuery-Abhängigkeit

jQuery-Migration

Bestehende jQuery-Plugins auf Alpine.js migrieren und Performance verbessern

Hyvä-Integration

Nahtlose CSP-konforme Integration in bestehende Hyvä-Theme-Strukturen

10. Zusammenfassung

Ein Alpine.js Rating-Widget ohne jQuery ist kein Kompromiss, sondern eine Verbesserung gegenüber jQuery-basierten Plugins. Der State lebt vollständig in x-data, der DOM bleibt serverseitig vollständig, und ARIA-Attribute machen das Widget für alle Nutzer zugänglich. Hover-State, Klick-Persistenz, localStorage-Integration und API-Anbindung passen in unter 80 Zeilen JavaScript – ohne externe Abhängigkeiten, ohne Build-Step, mit nativem Browser-fetch.

Für Hyvä-Themes-Projekte in Magento 2 ist Alpine.js die einzig sinnvolle Wahl: Es ist bereits geladen, es ist CSP-konform einsetzbar, und es schließt den Widerspruch zwischen jQuery-freiem Frontend und jQuery-abhängigen Widgets. Das hier vorgestellte Pattern – isolierter x-data-Scope, deklarative Bindings, async/await für API-Calls – lässt sich direkt auf andere interaktive Elemente übertragen und bildet eine solide Grundlage für den gesamten Alpine.js-Komponentenstack.

Alpine.js Rating-Widget — Das Wichtigste auf einen Blick

State-Management

Alles in x-data: rating, hoverRating, submitted, loading. Kein globaler State, kein Namespace-Konflikt bei mehreren Widgets auf einer Seite.

Hover ohne DOM-Traversal

Nur eine Variable hoverRating ändern – Alpine berechnet für jeden Stern automatisch die korrekte CSS-Klasse. Kein querySelectorAll, kein manelles Schleife.

Accessibility

role="radiogroup", aria-pressed, aria-label per x-bind und Pfeil-Tastaturnavigation per x-on:keydown – alles deklarativ im Template.

Hyvä & CSP

Alpine.js ist in Hyvä nativ enthalten. Inline-Script mit $hyvaCsp->registerInlineScript() registrieren. Kein jQuery nachladen, kein Performance-Verlust.

11. FAQ: Alpine.js Rating-Widget

1Kann Alpine.js jQuery für ein Rating-Widget vollständig ersetzen?
Ja. Alpine.js bietet alle nötigen Primitiven – x-data, x-on, x-bind und x-for. Hover-State, Klick-Persistenz, Accessibility und API-Anbindung lassen sich vollständig ohne jQuery umsetzen.
2Mehrere Rating-Widgets auf einer Seite ohne Konflikte?
Jedes x-data erzeugt einen isolierten Scope. Kein geteilter State, keine gegenseitige Beeinflussung – unabhängig davon, ob beide dieselbe Komponenten-Funktion nutzen.
3Mehrfaches Bewerten verhindern?
submitted = true nach Submit setzen und Wert in localStorage speichern. init() liest gespeicherten Wert aus und startet direkt im Read-only-Mode.
4CSP-konforme Integration in Hyvä?
Inline-Script ans Ende des .phtml-Templates, $hyvaCsp->registerInlineScript() aufrufen. Alpine.js ist global geladen, kein eigener Script-Tag nötig.
5Funktioniert das Widget ohne JavaScript?
Der DOM ist vollständig gerendert und lesbar. Interaktivität erfordert JS. Für Progressive Enhancement kann ein klassisches HTML-Form als Fallback dienen.
6Halbe Sterne für Durchschnittsanzeige?
starFill(i, value) gibt 'full', 'half' oder 'empty' zurück. SVG clipPath beschränkt den gelben Vordergrund auf die linke Hälfte des Stern-Icons.
7Bewertung an Magento REST-API senden?
Nativer fetch-POST an /rest/V1/reviews mit JSON-Body. async/await in der submit()-Methode. loading und submitted als State-Flags. Kein Axios nötig.
8Tastatursteuerung implementieren?
x-on:keydown.arrow-right und .arrow-left am Container. Alpine-Keyboard-Modifikatoren ersetzen manuellen addEventListener komplett.
9Rating-Wert per x-model an Hidden-Input binden?
<input type="hidden" x-model="rating" name="rating"> im x-data-Container – wird automatisch synchronisiert. Nützlich für klassische Form-Submissions ohne fetch.
10Widget ohne Backend testen?
fetch durch setTimeout-Promise ersetzen. Loading-, Success- und Error-State isoliert im Frontend testen, bevor der API-Endpoint existiert.