von der EAV-Attribut bis zur Alpine-State-Machine
Wer Swatches für konfigurierbare Produkte nur als Farb-Kästchen ohne durchdachtes State-Management umsetzt, baut sich Preis-, Galerie- und Bestandsfehler auf der Produktdetailseite ein. Eine saubere Alpine.js-State-Machine, ein passendes ViewModel und ein GraphQL-taugliches Datenmodell machen Hyvä-Swatches robust, barrierefrei und cachefreundlich zugleich.
Inhaltsverzeichnis
- 1. Wie Swatches in Hyvä technisch funktionieren
- 2. Datenmodell: Swatch-Attribute und EAV-Setup
- 3. Swatch-Rendering auf der Produktdetailseite
- 4. Alpine.js-State-Machine für die Optionsauswahl
- 5. GraphQL-Integration für konfigurierbare Optionen
- 6. Swatches im Kategorie-Grid
- 7. Fallback und Barrierefreiheit
- 8. Caching-Fallstricke bei Swatch-Wechsel
- 9. Eigene Swatch-Typen entwickeln
- 10. Zusammenfassung
- 11. FAQ
1. Wie Swatches in Hyvä technisch funktionieren
Swatches für konfigurierbare Produkte basieren im Kern weiterhin auf dem Magento-Core-Modul Magento_Swatches, das Attributdaten, Swatch-Typen und die Zuordnung zu konfigurierbaren Optionen verwaltet. Was sich in Hyvä fundamental ändert, ist die Rendering-Schicht: Statt des Knockout.js-basierten Magento_Swatches/js/swatch-renderer mit seiner jQuery-Widget-Fabrik kommt eine schlanke Kombination aus PHP-ViewModel und Alpine.js-Komponente zum Einsatz. Die Datenhaltung im Backend bleibt identisch, nur der Client-seitige Rendering- und Interaktionslayer wird komplett ersetzt.
Das Grundprinzip von Hyvä-Swatches ist bewusst serverseitig verankert: Alle verfügbaren Optionen, deren Swatch-Werte (Hex-Farbe, Bild-URL oder Text) und die Zuordnungstabelle von Optionskombination zu Kind-SKU werden beim Seitenaufbau als JSON in die Seite eingebettet. Alpine.js übernimmt danach ausschließlich die Interaktionslogik im Browser, ohne dass ein weiterer Server-Roundtrip für die reine Anzeige nötig ist. Das reduziert die Menge an JavaScript drastisch gegenüber der Knockout-Variante und macht das Verhalten der Optionsauswahl leicht nachvollziehbar, weil der komplette Zustand in einem einzigen x-data-Objekt sichtbar ist.
Wichtig für jede Implementierung von Swatches für konfigurierbare Produkte ist, dass die Optionsauswahl ohne jQuery und ohne UI-Components auskommt. Die Hyvä-Kompatibilitätsschicht verzichtet auf Widget-Initialisierung per data-mage-init und ersetzt sie durch deklarative x-data-Attribute direkt im phtml-Template. Dadurch entfällt auch die asynchrone Widget-Bootstrapping-Phase, die in Luma-Themes regelmäßig zu Flackern der Swatch-Buttons beim ersten Rendern führt.
2. Datenmodell: Swatch-Attribute und EAV-Setup
Magento unterscheidet bei Swatches für konfigurierbare Produkte zwischen zwei Attribut-Frontend-Inputs: swatch_visual für Farb- und Bild-Swatches sowie swatch_text für reine Text-Swatches. Beide werden in der EAV-Attributtabelle als normale select- oder multiselect-Attribute abgelegt, zusätzlich aber in eav_attribute_option_swatch mit dem eigentlichen Swatch-Wert verknüpft: bei visuellen Swatches ein Hex-Code oder ein Bildpfad, bei Text-Swatches der sichtbare Label-Text selbst. Diese Trennung ist entscheidend, weil sie später bestimmt, welcher Rendering-Zweig im ViewModel greift.
Ein neues Swatch-Attribut legt man über ein deklaratives Setup-Skript oder eine Setup/Patch/Data-Klasse an, nicht über InstallData-Skripte alter Bauart. Die Konfiguration muss swatch_input_type auf visual oder text setzen und das Attribut zusätzlich als used_in_product_listing markieren, damit es im Kategorie-Grid ohne teuren Extra-Join verfügbar ist. Ohne diese Markierung lädt der Produkt-Collection-Loader das Attribut nicht in die Listing-Collection, und die Swatches im Grid müssten pro Produkt einzeln nachgeladen werden, was die Kategorie-Seite spürbar verlangsamt.
<?php
declare(strict_types=1);
namespace Mironsoft\Swatches\Setup\Patch\Data;
use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Catalog\Model\Product;
/**
* Creates the "material" visual swatch attribute for configurable products.
*/
final class CreateMaterialSwatchAttribute implements DataPatchInterface
{
/**
* Injects the module data setup and the EAV setup factory.
*
* @param ModuleDataSetupInterface $moduleDataSetup Core setup connection.
* @param EavSetupFactory $eavSetupFactory Factory for EAV setup helper.
*/
public function __construct(
private readonly ModuleDataSetupInterface $moduleDataSetup,
private readonly EavSetupFactory $eavSetupFactory,
) {
}
/**
* Runs the data patch and creates the swatch attribute.
*
* @return void
*/
public function apply(): void
{
/** @var EavSetup $eavSetup */
$eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
$eavSetup->addAttribute(
Product::ENTITY,
'material',
[
'type' => 'int',
'label' => 'Material',
'input' => 'select',
'source' => \Magento\Eav\Model\Entity\Attribute\Source\Table::class,
'swatch_input_type' => 'visual',
'frontend_input_renderer' => \Magento\Swatches\Model\Product\Attribute\Frontend\Renderer::class,
'global' => \Magento\Catalog\Model\ResourceModel\Eav\Attribute::SCOPE_GLOBAL,
'visible' => true,
'required' => false,
'user_defined' => true,
'searchable' => false,
'filterable' => true,
'comparable' => false,
'visible_on_front' => true,
'used_in_product_listing' => true,
'unique' => false,
'apply_to' => Product\Type::TYPE_SIMPLE,
'is_configurable' => true,
]
);
}
/**
* Declares patch dependencies.
*
* @return array<int, string>
*/
public static function getDependencies(): array
{
return [];
}
/**
* Declares aliases for this patch.
*
* @return array<int, string>
*/
public function getAliases(): array
{
return [];
}
}
Auf der konfigurierbaren Produktebene wird jedes Swatch-Attribut zusätzlich als configurable_option in catalog_product_super_attribute registriert. Diese Tabelle bildet die Brücke zwischen dem Parent-Produkt und den Kind-Produkten: Für jede Attributkombination existiert ein Eintrag in catalog_product_super_link, der auf die passende Kind-SKU verweist. Genau diese Zuordnungstabelle wird später als JSON-Payload für die Alpine-State-Machine seriealisiert, damit im Browser ohne weiteren Serverzugriff die richtige Kind-SKU zur gewählten Optionskombination gefunden werden kann.
3. Swatch-Rendering auf der Produktdetailseite
Für das Rendering von Swatches für konfigurierbare Produkte auf der PDP ist ein dediziertes ViewModel die richtige Wahl gegenüber einer klassischen Block-Klasse, weil es sich sauber über ArgumentInterface in die Layout-XML einbinden lässt und keine Vererbungskette mit dem Core-Block Configurable erzwingt. Das ViewModel liest die konfigurierbaren Attribute des aktuellen Produkts, ermittelt für jedes Attribut den Swatch-Typ und liefert ein sauber typisiertes Array an das Template, statt Rendering-Logik in PHP-Helfer-Methoden zu verstecken.
Im phtml-Template selbst werden Farb-Swatches als quadratische Buttons mit background-color aus dem Hex-Wert gerendert, Bild-Swatches als <button> mit Hintergrundbild und Text-Swatches als beschriftete Pillen. Entscheidend ist korrektes Escaping: Hex-Werte und Bildpfade kommen aus der Datenbank und dürfen niemals ungeprüft in style-Attribute geschrieben werden. Der Hyvä-Standardweg nutzt $escaper->escapeHtmlAttr() für den Style-Wert und $escaper->escapeUrl() für Bildpfade, damit auch bei manipulierten Attributwerten kein Style-Injection möglich ist.
<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Mironsoft\Swatches\ViewModel\ConfigurableSwatches $swatchesViewModel */
$swatchesViewModel = $block->getData('swatchesViewModel');
$options = $swatchesViewModel->getSwatchOptions($product);
?>
<div
x-data="configurableSwatches({
options: <?= /* @noEscape */ $swatchesViewModel->getOptionsJson($product) ?>,
indexedSkus: <?= /* @noEscape */ $swatchesViewModel->getIndexedSkusJson($product) ?>,
productId: <?= (int) $product->getId() ?>
})"
class="not-prose"
>
<?php foreach ($options as $option): ?>
<fieldset class="mb-6">
<legend class="text-sm font-semibold text-slate-700 mb-2">
<?= $escaper->escapeHtml($option['label']) ?>
</legend>
<div class="flex flex-wrap gap-2">
<?php foreach ($option['values'] as $value): ?>
<?php if ($value['swatch_type'] === 'color'): ?>
<button
type="button"
class="w-9 h-9 rounded-full border-2 transition-all"
style="background-color: <?= $escaper->escapeHtmlAttr($value['swatch_data']) ?>;"
:class="isSelected(<?= (int) $option['attribute_id'] ?>, <?= (int) $value['value_index'] ?>) ? 'border-orange-600 scale-110' : 'border-slate-200'"
:aria-pressed="isSelected(<?= (int) $option['attribute_id'] ?>, <?= (int) $value['value_index'] ?>).toString()"
aria-label="<?= $escaper->escapeHtmlAttr($value['label']) ?>"
@click="selectOption(<?= (int) $option['attribute_id'] ?>, <?= (int) $value['value_index'] ?>)"
></button>
<?php elseif ($value['swatch_type'] === 'image'): ?>
<button
type="button"
class="w-12 h-12 rounded-lg border-2 bg-cover bg-center"
style="background-image: url('<?= $escaper->escapeUrl($value['swatch_data']) ?>');"
:class="isSelected(<?= (int) $option['attribute_id'] ?>, <?= (int) $value['value_index'] ?>) ? 'border-orange-600' : 'border-slate-200'"
aria-label="<?= $escaper->escapeHtmlAttr($value['label']) ?>"
@click="selectOption(<?= (int) $option['attribute_id'] ?>, <?= (int) $value['value_index'] ?>)"
></button>
<?php else: ?>
<button
type="button"
class="px-3 py-2 rounded-lg border text-sm font-medium"
:class="isSelected(<?= (int) $option['attribute_id'] ?>, <?= (int) $value['value_index'] ?>) ? 'border-orange-600 text-orange-700 bg-orange-50' : 'border-slate-200 text-slate-700'"
@click="selectOption(<?= (int) $option['attribute_id'] ?>, <?= (int) $value['value_index'] ?>)"
>
<?= $escaper->escapeHtml($value['label']) ?>
</button>
<?php endif; ?>
<?php endforeach; ?>
</div>
</fieldset>
<?php endforeach; ?>
</div>
Ein Detail, das bei Swatches für konfigurierbare Produkte häufig übersehen wird: Die Reihenfolge der Optionsgruppen im ViewModel muss der Reihenfolge in catalog_product_super_attribute.position folgen, sonst wechselt die Darstellung von Farbe und Größe je nach Produkt unvorhersehbar. Das ViewModel sollte diese Sortierung explizit übernehmen, statt sich auf die zufällige Reihenfolge der EAV-Collection zu verlassen.
4. Alpine.js-State-Machine für die Optionsauswahl
Das Herzstück jeder Hyvä-Implementierung von Swatches für konfigurierbare Produkte ist die Alpine-Komponente configurableSwatches. Ihr x-data-Objekt hält selectedOptions als Map von Attribut-ID zu gewähltem Options-Index, dazu den aktuell aufgelösten currentProduct mit Preis, Bestand und Galerie-Bildern. Ein $watch auf selectedOptions löst bei jeder Änderung eine Neuberechnung aus: Zuerst wird geprüft, ob alle Pflichtoptionen ausgewählt sind, dann wird anhand der im JSON eingebetteten Zuordnungstabelle die passende Kind-SKU ermittelt, ganz ohne Server-Roundtrip für den Regelfall.
Für Preis-, Galerie- und Bestandsdaten reicht die eingebettete Zuordnungstabelle in der Regel aus, weil sie beim Seitenaufbau bereits alle Kind-Produktdaten enthält. Ein fetch gegen den Configurable-Endpoint ist nur dann nötig, wenn Live-Bestandsdaten oder tier-price-abhängige Sonderpreise nicht statisch eingebettet werden dürfen, etwa bei sehr volatilen Lagerbeständen. In diesem Fall ruft die State-Machine /graphql mit einer schlanken Query auf, die ausschließlich Preis und Bestand der ermittelten Kind-SKU nachlädt, statt die komplette Produktseite neu zu laden.
Das Preis- und Galerie-Update erfolgt rein reaktiv über Alpine-Bindings: x-text="formattedPrice" für die Preisanzeige und x-show in Kombination mit einer indexbasierten Bildliste für die Galerie. Wichtig ist, niemals mit einem literalen Mustache wie geschweiften Doppelklammern zu arbeiten, sondern konsequent x-text zu verwenden, da Alpine kein Mustache-Templating im DOM auswertet und ein solcher Text sonst unverändert im Browser sichtbar bliebe.
// Alpine.js component for configurable product swatches
// Registered globally so the phtml template can reference it directly
document.addEventListener('alpine:init', () => {
Alpine.data('configurableSwatches', (config) => ({
options: config.options,
indexedSkus: config.indexedSkus,
productId: config.productId,
selectedOptions: {},
currentProduct: null,
isLoadingStock: false,
init() {
// Watch for changes and resolve the matching child SKU
this.$watch('selectedOptions', () => {
this.resolveCurrentProduct();
});
},
selectOption(attributeId, valueIndex) {
this.selectedOptions = { ...this.selectedOptions, [attributeId]: valueIndex };
},
isSelected(attributeId, valueIndex) {
return this.selectedOptions[attributeId] === valueIndex;
},
allOptionsSelected() {
return this.options.every((option) => this.selectedOptions[option.attribute_id] !== undefined);
},
// Build the lookup key the same way the backend built indexedSkus
buildLookupKey() {
return this.options
.map((option) => this.selectedOptions[option.attribute_id])
.join(',');
},
resolveCurrentProduct() {
if (!this.allOptionsSelected()) {
this.currentProduct = null;
return;
}
const key = this.buildLookupKey();
const match = this.indexedSkus[key];
if (!match) {
this.currentProduct = null;
return;
}
this.currentProduct = match;
this.maybeRefreshLiveStock(match.sku);
},
get formattedPrice() {
if (!this.currentProduct) {
return '';
}
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(this.currentProduct.price);
},
get galleryImages() {
return this.currentProduct ? this.currentProduct.images : [];
},
get inStock() {
return this.currentProduct ? this.currentProduct.stock_status === 'IN_STOCK' : false;
},
// Only used when live stock data must not be baked into the page cache
async maybeRefreshLiveStock(sku) {
if (!this.currentProduct || !this.currentProduct.requires_live_stock) {
return;
}
this.isLoadingStock = true;
try {
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: 'query($sku: String!) { products(filter: { sku: { eq: $sku } }) { items { stock_status only_x_left_in_stock } } }',
variables: { sku },
}),
});
const payload = await response.json();
const item = payload.data.products.items[0];
this.currentProduct.stock_status = item.stock_status;
this.currentProduct.only_x_left = item.only_x_left_in_stock;
} finally {
this.isLoadingStock = false;
}
},
}));
});
5. GraphQL-Integration für konfigurierbare Optionen
Für Headless-Frontends oder für den Fall, dass die serverseitig eingebettete Zuordnungstabelle nicht ausreicht, liefert Magento über den configurableProductOptions-Zweig der products-Query alle Attributwerte samt zugehöriger Swatch-Daten. Die Query gibt für jedes konfigurierbare Attribut die values mit value_index, label und den passenden Swatch-Typ zurück, sodass sich Swatches für konfigurierbare Produkte auch in einem PWA- oder Headless-Kontext ohne zusätzlichen REST-Aufruf rendern lassen.
Der zweite wichtige Baustein ist configurableProductOptionsSelection: Diese Query nimmt die aktuell gewählten value_index-Werte entgegen und liefert zurück, welche Kombinationen noch verfügbar sind und welche Kind-SKU zur vollständigen Auswahl gehört. Ein eigener Resolver kann diese Logik serverseitig cachen, weil die Zuordnung von Optionskombination zu SKU sich nur bei Änderungen am Produktmodell ändert, nicht bei jedem Seitenaufruf.
query ConfigurableSwatchOptions($sku: String!) {
products(filter: { sku: { eq: $sku } }) {
items {
sku
name
... on ConfigurableProduct {
configurable_options {
attribute_code
label
position
values {
value_index
label
swatch_data {
... on ImageSwatchData {
thumbnail
}
... on ColorSwatchData {
value
}
... on TextSwatchData {
value
}
}
}
}
variants {
product {
sku
price_range {
minimum_price {
final_price {
value
currency
}
}
}
stock_status
only_x_left_in_stock
media_gallery {
url
}
}
attributes {
code
value_index
}
}
}
}
}
}
Ein häufiges Missverständnis: variants liefert nicht automatisch nur verfügbare Kombinationen. Der Resolver muss stock_status jedes Varianten-Produkts explizit auswerten, um ausverkaufte Kombinationen im Frontend als deaktiviert zu markieren, statt sie kommentarlos anklickbar zu lassen. Ein eigener kleiner GraphQL-Resolver, der die verfügbaren SKU-Kombinationen je Optionsauswahl vorab aggregiert, spart im Frontend eine clientseitige Kreuzprodukt-Berechnung über alle Attributkombinationen.
6. Swatches im Kategorie-Grid
Swatches für konfigurierbare Produkte beschränken sich in der Praxis nicht auf die PDP. Im Kategorie-Grid zeigt eine Mini-Swatch-Liste unterhalb des Produktbilds die verfügbaren Farboptionen, oft begrenzt auf vier bis fünf sichtbare Punkte plus einen Zähler für weitere Optionen. Voraussetzung dafür ist, dass das Swatch-Attribut wie in Abschnitt 2 beschrieben als used_in_product_listing markiert ist, damit die Produkt-Collection der Kategorieseite die Swatch-Werte ohne zusätzlichen Join pro Produkt mitliefert.
Ein Hover-Preview-Muster, bei dem das Hauptbild der Produktkarte beim Überfahren eines Farb-Swatches auf das entsprechende Variantenbild wechselt, verbessert die Konversion spürbar, weil Kunden Farboptionen sehen, ohne die Karte zu verlassen. Technisch reicht dafür ein x-data auf Kartenebene mit einem activeImage, das per @mouseenter auf dem Swatch-Button gesetzt wird, kombiniert mit einer kleinen, im Grid-Loader vorbereiteten Zuordnung von Swatch-Wert zu Bild-URL.
Lazy-Loading ist bei Hyvä-Swatches im Grid ein doppeltes Thema: Zum einen sollten die Vorschaubilder pro Swatch-Option nicht alle beim initialen Seitenaufbau geladen werden, sondern erst bei @mouseenter beziehungsweise beim ersten Touch-Tap nachgeladen werden, um die initiale Payload der Kategorieseite klein zu halten. Zum anderen muss das native loading="lazy"-Attribut auf den Standard-Produktbildern bestehen bleiben, damit Swatch-Vorschaubilder unterhalb des sichtbaren Bereichs nicht die Ladezeit der Kategorie-Seite verschlechtern.
| Aufgabe | Naiver Ansatz | Empfohlenes Hyvä-Pattern | Vorteil |
|---|---|---|---|
| Optionsauswahl auf der PDP | Klassisches <select>-Dropdown | Visuelle Swatch-Buttons mit Alpine-State | Bessere Konversion, direkte visuelle Rückmeldung |
| Preis-/Galerie-Update | Voller Seiten-Reload nach Optionswechsel | Alpine-State-Machine mit lokalem Lookup | Sofortiges Update ohne Serverlast |
| Fehlendes Swatch-Bild | Leerer oder kaputter Button | Text-Swatch-Fallback mit Label | Immer bedienbare Option, keine toten Buttons |
| Preis-/Bestandscache | Statischer Preis direkt im Full Page Cache | Private-Content-Section für variable Werte | Korrekte Preise trotz FPC |
| Zusätzliches Swatch-Attribut | Hartcodierter if-Block im Template | Erweiterbares ViewModel mit Swatch-Typ-Mapping | Neue Swatch-Typen ohne Template-Fork |
7. Fallback und Barrierefreiheit
Ein robustes Fallback-Konzept gehört zu jeder Implementierung von Swatches für konfigurierbare Produkte: Fehlt einem Bild-Swatch der hinterlegte Bildpfad, etwa weil ein Redakteur ihn beim Anlegen der Option vergessen hat, muss das ViewModel automatisch auf die Text-Swatch-Darstellung mit dem Options-Label zurückfallen, statt einen leeren oder gebrochenen Button anzuzeigen. Diese Fallback-Logik gehört ins ViewModel, nicht ins Template, damit sie zentral getestet und in beiden Vendor-Varianten identisch gepflegt werden kann.
Bei Farb-Swatches ist eine Kontrastprüfung sinnvoll: Sehr helle Farbwerte wie Weiß oder Creme brauchen einen sichtbaren Rahmen, damit der Button auf hellem Hintergrund nicht optisch verschwindet. Ein einfacher Luminanz-Check im ViewModel, der bei einem berechneten Helligkeitswert oberhalb eines Schwellwerts automatisch eine zusätzliche Border-Klasse setzt, verhindert unsichtbare Swatch-Buttons ohne manuelle Pflege pro Farboption.
Für Screenreader-Nutzer sind aria-pressed und aria-label an jedem Swatch-Button Pflicht, wie im Code-Beispiel aus Abschnitt 3 gezeigt. aria-pressed muss dynamisch an den Alpine-Zustand gebunden sein, damit ein Screenreader den aktuell gewählten Farb- oder Bild-Swatch korrekt als ausgewählt ansagt. Der aria-label sollte immer den vollen, für Menschen lesbaren Optionsnamen enthalten, nicht nur einen internen Wert-Index, da reine Farbnamen wie "Petrolblau" für Screenreader-Nutzer die einzige Information über die gewählte Option sind.
8. Caching-Fallstricke bei Swatch-Wechsel
Der Full Page Cache von Magento speichert die gerenderte PDP inklusive des initial ausgewählten Swatch-Zustands. Für Swatches für konfigurierbare Produkte bedeutet das: Preis, Bestand und Galerie-Bilder des zuerst angezeigten Kind-Produkts dürfen niemals fest in den gecachten HTML-Block geschrieben werden, wenn sie sich je nach Kundensegment oder Lagerstand ändern können. Andernfalls sehen alle Besucher denselben eingefrorenen Preis, unabhängig von tatsächlicher Verfügbarkeit oder kundenspezifischen Sonderpreisen.
Der korrekte Weg führt über Private-Content-Sections beziehungsweise über clientseitig nachgeladene Daten, die außerhalb des FPC-Cache-Schlüssels liegen. Da die Alpine-State-Machine aus Abschnitt 4 die Zuordnungstabelle ohnehin als eingebettetes JSON mitführt, bleibt der FPC-Eintrag für alle Optionskombinationen identisch cachefähig, während die tatsächliche Preis- und Bestandsanzeige rein clientseitig aus dem JSON berechnet wird. Nur wenn Live-Bestandsdaten aus einem externen System nachgeladen werden müssen, kommt der in Abschnitt 4 gezeigte Fetch gegen den GraphQL-Endpoint zum Einsatz, dessen Antwort selbst wiederum nicht im FPC landen darf.
Ein klassischer Fehler in bestehenden Hyvä-Projekten: Ein Entwickler bindet den initial gewählten Swatch-Zustand serverseitig in Abhängigkeit vom Query-Parameter oder von der Session ein und bricht damit unbeabsichtigt die FPC-Cachefähigkeit der gesamten Seite, weil Varnish oder der Magento-eigene FPC dann pro Kombination einen eigenen Cache-Eintrag anlegen müsste. Die sichere Regel lautet: Der initiale HTML-Zustand zeigt immer die Default-Optionskombination, jede abweichende Auswahl wird ausschließlich clientseitig über Alpine.js aufgelöst.
9. Eigene Swatch-Typen entwickeln
Über die Standard-Typen Farbe, Bild und Text hinaus lassen sich eigene Swatch-Darstellungen für Swatches für konfigurierbare Produkte ergänzen, etwa ein Muster-Swatch für Textilien oder ein Material-Swatch mit kombinierter Textur und Label. Die Grundlage dafür ist ein zusätzliches EAV-Attribut wie in Abschnitt 2, dessen swatch_input_type weiterhin visual bleibt, dessen Darstellung im Frontend aber über einen eigenen Rendering-Zweig im ViewModel erweitert wird.
Die Erweiterung erfolgt über eine Swatch-Typ-Map im ViewModel, die den internen Typ-Schlüssel auf ein Template-Partial abbildet. Statt eines wachsenden if/elseif-Blocks im phtml-Template registriert man neue Swatch-Typen deklarativ über di.xml als zusätzlichen Eintrag in einem virtuellen Type für die Swatch-Typ-Map, sodass Drittmodule eigene Swatch-Darstellungen hinzufügen können, ohne das Kern-Template zu überschreiben.
{
"sku": "SHIRT-BLUE-M",
"configurable_options": [
{
"attribute_code": "material",
"label": "Material",
"position": 1,
"values": [
{
"value_index": 12,
"label": "Baumwolle",
"swatch_type": "material",
"swatch_data": {
"texture_url": "/media/swatches/cotton.png",
"pattern": "plain-weave"
}
},
{
"value_index": 13,
"label": "Leinen",
"swatch_type": "material",
"swatch_data": {
"texture_url": "/media/swatches/linen.png",
"pattern": "basket-weave"
}
}
]
},
{
"attribute_code": "color",
"label": "Farbe",
"position": 2,
"values": [
{
"value_index": 24,
"label": "Petrolblau",
"swatch_type": "color",
"swatch_data": "#0e4f56"
}
]
}
],
"indexed_skus": {
"12,24": { "sku": "SHIRT-BLUE-M-COTTON", "price": 39.9, "stock_status": "IN_STOCK" },
"13,24": { "sku": "SHIRT-BLUE-M-LINEN", "price": 44.9, "stock_status": "OUT_OF_STOCK" }
}
}
Für den Material-Swatch aus dem Beispiel rendert das ViewModel einen Button mit Hintergrundtextur statt Flächenfarbe und ergänzt zusätzlich das Label sichtbar unter dem Swatch, weil reine Texturen ohne Beschriftung oft schwer unterscheidbar sind. Diese Kombination aus visuellem Reiz und Textlabel ist bei Material- und Muster-Swatches nahezu immer sinnvoll, während sie bei einfachen Farb-Swatches meist verzichtbar bleibt.
Mironsoft
Hyvä-Frontend-Entwicklung für Magento 2
Swatches für konfigurierbare Produkte richtig umgesetzt?
Wir bauen Hyvä-Swatches mit sauberem ViewModel, robuster Alpine-State-Machine und cachefreundlichem Datenmodell, inklusive GraphQL-Anbindung und Barrierefreiheit nach WCAG-Vorgaben.
Swatch-Konzeption
EAV-Setup, ViewModel-Architektur und Swatch-Typ-Erweiterungen
Alpine-State-Machine
Preis-, Galerie- und Bestandsupdate ohne Reload und ohne jQuery
Caching & GraphQL
FPC-sichere Architektur und Resolver für konfigurierbare Optionen
10. Zusammenfassung
Swatches für konfigurierbare Produkte in Hyvä bestehen aus drei zusammenspielenden Schichten: dem unveränderten Magento-Datenmodell aus Magento_Swatches und EAV-Attributen, einem ViewModel, das die Swatch-Typen sauber trennt und sicher escaped, und einer Alpine.js-State-Machine, die Preis, Galerie und Bestand ohne Server-Roundtrip aktualisiert. Diese Kombination ersetzt den Knockout-basierten Swatch-Renderer vollständig und reduziert gleichzeitig die JavaScript-Menge auf der Seite erheblich.
Barrierefreiheit über aria-pressed und aria-label, ein Text-Swatch-Fallback bei fehlenden Bildern und eine FPC-sichere Architektur ohne serverseitig eingebettete variable Preise gehören zu jeder produktionsreifen Umsetzung von Hyvä-Swatches. Wer die GraphQL-Integration für Headless-Szenarien vorbereitet und das ViewModel von Anfang an für eigene Swatch-Typen erweiterbar baut, spart sich spätere Template-Forks bei neuen Anforderungen wie Material- oder Muster-Swatches.
Swatches für konfigurierbare Produkte in Hyvä: Das Wichtigste auf einen Blick
Datenmodell
swatch_visual und swatch_text als EAV-Attribute, verknüpft über eav_attribute_option_swatch. used_in_product_listing für performantes Grid-Rendering.
Rendering
ViewModel statt Block-Klasse, korrektes Escaping von Hex-Werten und Bildpfaden, sauber getrennte Swatch-Typ-Zweige im Template.
Alpine-State-Machine
selectedOptions-Map mit $watch, lokaler Lookup gegen eingebettetes JSON, optionaler Fetch für Live-Bestandsdaten.
Caching & Barrierefreiheit
Keine variablen Preise im FPC-Cache-Schlüssel. aria-pressed/aria-label und Text-Swatch-Fallback für jeden Button.