Inhoud van dit artikel
- De twee regels achter elk Hyvä UI-component
- Stap 1: design tokens en Tailwind-config
- Stap 2: de markup van het component
- Stap 3: het Alpine-component schrijven
- Stap 4: het component aanroepen
- Stap 5: varianten via Tailwind, niet via JavaScript
- Product cards: meer dan een afbeelding en een prijs
- PLP-filters: conversie zit in de snelheid
- Productgalerij op de PDP
- Checkout-UI: Hyvä UI, Hyvä Checkout of allebei?
- Integratie in een bestaand Hyvä-project
- Wanneer je beter niet zelf bouwt in Alpine
- Checklist: component klaar voor productie?
- Wat levert dit op?
Hyvä UI levert tientallen kant-en-klare componenten, maar vroeg of laat wil je iets dat er niet tussen zit. Dan bouw je zelf een component in precies dezelfde stijl: Tailwind-classes voor de vorm, Alpine.js voor interactie, geen extra JavaScript-bundle. Dit artikel laat stap voor stap zien hoe, en past dezelfde aanpak daarna toe op de plekken waar het geld verdiend wordt: product cards, PLP-filters, de productgalerij en de checkout.
Wat Hyvä UI is en wat de licentie kost, staat in Wat is Hyvä UI?. Hier gaat het om bouwen.
De twee regels achter elk Hyvä UI-component
- HTML-first. Structuur en stijl zitten in de markup, niet in een JavaScript-module.
- Alpine.js alleen voor interactie. State, events en eenvoudige DOM-wijzigingen. Business logic hoort in PHP: een ViewModel of block, niet in de browser.
Houd je die twee regels aan, dan gedraagt je eigen component zich als de meegeleverde componenten. Dat is goed voor consistentie in het team en voor snelheid. De winst verdwijnt zodra je Alpine inzet voor iets wat CSS al kan, of een externe library laadt voor iets triviaals.
Elk component heeft dezelfde vier lagen:
| Laag | Verantwoordelijkheid | Technologie |
|---|---|---|
| Markup | Structuur en semantiek | HTML in een .phtml-template |
| Stijl | Visuele presentatie | Tailwind CSS |
| Interactie | State en events | Alpine.js |
| Data | Inhoud en configuratie | PHP: ViewModel, block, layout XML |
Stap 1: design tokens en Tailwind-config
Kleuren, spacing en typografie horen in je theme-configuratie, niet hard-coded in een component. Gebruik bg-primary, niet bg-[#1a56db].
Waar die tokens staan, hangt af van je Hyvä-versie:
- Hyvä 1.4 en nieuwer (Tailwind v4): tokens in
web/tailwind/hyva.config.json, omgezet naar CSS-variabelen in een@theme-blok metnpx hyva-tokens. - Oudere themes (Tailwind v3): tokens in
theme.extendvanweb/tailwind/tailwind.config.js.
Een minimale tokenset in hyva.config.json:
{
"tokens": {
"values": {
"color": {
"primary": {
"lighter": "#3f83f8",
"DEFAULT": "#1a56db",
"darker": "#1e429f"
},
"secondary": {
"DEFAULT": "#0e9f6e"
}
}
}
}
}
De volledige uitleg over tokens, Tailwind v3 versus v4 en de koppeling met Figma staat in design tokens koppelen aan Tailwind.
Stap 2: de markup van het component
We bouwen een notification banner: een balk met een bericht die de bezoeker kan wegklikken en die optioneel vanzelf verdwijnt. Simpel genoeg om te volgen, compleet genoeg om de patronen te laten zien. Hyvä UI heeft zelf een Notification-component; kijk altijd eerst of je kunt uitbreiden in plaats van opnieuw bouwen.
Plaats de template in je theme, bijvoorbeeld app/design/frontend/Acme/default/Magento_Theme/templates/html/notification-banner.phtml:
<?php
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Magento\Framework\View\Element\Template $block */
$message = (string) $block->getData('message');
$type = (string) ($block->getData('type') ?: 'info');
$autoClose = (int) $block->getData('auto_close');
?>
<div
x-data="notificationBanner"
x-show="visible"
x-transition.opacity.duration.200ms
data-type="<?= $escaper->escapeHtmlAttr($type) ?>"
data-auto-close="<?= $autoClose ?>"
:class="variantClasses"
role="status"
class="relative flex items-center justify-between gap-4 rounded-lg px-4 py-3 text-sm font-medium shadow-md"
>
<p class="min-w-0 truncate"><?= $escaper->escapeHtml($message) ?></p>
<button
type="button"
@click="close"
class="shrink-0 rounded p-1 hover:bg-white/20 focus:outline-none focus:ring-2 focus:ring-white/50"
aria-label="<?= $escaper->escapeHtmlAttr(__('Close')) ?>"
>
<svg class="h-4 w-4" aria-hidden="true" viewBox="0 0 20 20" fill="currentColor">
<path d="M4.3 4.3a1 1 0 0 1 1.4 0L10 8.6l4.3-4.3a1 1 0 1 1 1.4 1.4L11.4 10l4.3 4.3a1 1 0 0 1-1.4 1.4L10 11.4l-4.3 4.3a1 1 0 0 1-1.4-1.4L8.6 10 4.3 5.7a1 1 0 0 1 0-1.4z"/>
</svg>
</button>
</div>
Let op wat er niet in staat: geen inline JavaScript-expressies zoals open = !open en geen hex-kleuren. De tekst komt server-side uit PHP en wordt ge-escaped. Configuratie gaat via data-*-attributen.
Stap 3: het Alpine-component schrijven
Hyvä levert een standaard- en een CSP-variant van de theme. In de CSP-variant draait de CSP-build van Alpine, die geen unsafe-eval nodig heeft maar wel strenger is: geen inline objecten in x-data, geen negaties of toewijzingen in attributen, geen argumenten in method-calls en geen x-model. Schrijf je componenten meteen zo, dan werken ze in beide varianten.
Onderaan dezelfde template:
<script>
function notificationBanner() {
return {
visible: true,
type: 'info',
init() {
this.type = this.$el.dataset.type || 'info';
const autoClose = parseInt(this.$el.dataset.autoClose || '0', 10);
if (autoClose > 0) {
this.timer = setTimeout(() => this.close(), autoClose);
}
},
destroy() {
clearTimeout(this.timer);
},
close() {
this.visible = false;
},
variantClasses() {
return {
info: 'bg-primary text-white',
success: 'bg-secondary text-white',
error: 'bg-red-600 text-white',
warning: 'bg-yellow-400 text-gray-900',
}[this.type];
},
};
}
window.addEventListener('alpine:init', () => Alpine.data('notificationBanner', notificationBanner), {once: true});
</script>
<?php $hyvaCsp->registerInlineScript() ?>
Wat hier gebeurt:
Alpine.data()in eenalpine:init-listener. Zo registreer je een herbruikbaar component, en in de template verwijs je ernaar zonder haakjes:x-data="notificationBanner".init()endestroy()zijn Alpine-hooks.init()voor timers en listeners,destroy()om ze op te ruimen als het element uit de DOM verdwijnt.$hyvaCsp->registerInlineScript()registreert het inline script voor de Content Security Policy van Magento. Zonder deze regel blokkeert een strikte CSP je script.
Geen bundler, geen classes, geen dependency. De functie geeft een data-object terug, verder niets.
Stap 4: het component aanroepen
Via layout XML plaats je het block waar je het nodig hebt, met de configuratie als argumenten:
<referenceContainer name="page.top">
<block name="acme.notification.banner"
template="Magento_Theme::html/notification-banner.phtml">
<arguments>
<argument name="message" xsi:type="string" translate="true">Bestel vóór 22:00, morgen in huis.</argument>
<argument name="type" xsi:type="string">success</argument>
<argument name="auto_close" xsi:type="number">8000</argument>
</arguments>
</block>
</referenceContainer>
Content die server-side wordt gegenereerd, blijft zo in PHP en vervuilt je Alpine-markup niet. Meer Alpine-patronen in deze stijl vind je in ons artikel over Alpine-componenten in Hyvä.
Stap 5: varianten via Tailwind, niet via JavaScript
De vier varianten hierboven komen uit één method die een class-string teruggeeft. Er is geen JavaScript dat stijlen manipuleert, alleen een keuze uit Tailwind-classes.
Eén aandachtspunt: classes die alleen in JavaScript-strings staan, moet Tailwind wel kunnen vinden. Staan ze letterlijk in de .phtml, zoals hier, dan pikt de scan ze op. Bouw je classnamen dynamisch op ('bg-' + kleur), dan vindt Tailwind ze niet en ontbreken ze in productie. Schrijf classnamen daarom altijd voluit.
Product cards: meer dan een afbeelding en een prijs
Dezelfde aanpak geldt voor de componenten op de categoriepagina (PLP) en productpagina (PDP), waar bezoekers afhaken of kopen. Hyvä UI heeft hiervoor een Product-card met swatches, plus losse componenten voor Swatches, Ajax-ATC en Sticky-ATC. Begin daar en pas aan; bouw alleen zelf wat ontbreekt.
De Luma product card sleept veel mee: prijsboxen via Knockout, swatches via RequireJS-modules, lazy loading via jQuery-plugins. Een HTML-first card zet de prijs en naam gewoon in de HTML en gebruikt Alpine alleen voor swatch-selectie of quick-add.
Wat een goede product card doet
- Toont prijs en kortingsindicator in de server-HTML, zonder JavaScript-waterval
- Laadt afbeeldingen lazy, met correcte
width- enheight-attributen tegen layout shift - Geeft direct visuele feedback bij swatch-selectie, zonder round-trip
- Is met het toetsenbord te bedienen en heeft correcte
aria-labels - Werkt op 320 pixels breed net zo goed als op een groot scherm
- Laadt de eerste rij afbeeldingen boven de vouw niet lazy, want die bepalen je LCP
PLP-filters: conversie zit in de snelheid
Filters zijn het meest onderschatte conversie-element op een categoriepagina. Als de filterbalk traag reageert of op mobiel onbruikbaar is, gaat de bezoeker weg, hoe goed je productdata ook is.
De uitdaging is architectuur: filters houden state bij (geselecteerde waarden), werken de URL bij (deelbaar en meetbaar) en verversen de productgrid. Hyvä UI levert het Category-filter-component in een standaardvariant en een variant voor Elasticsuite. Hoe je state en URL afhandelt, hangt af van je setup: klassieke page loads met nette URL's, of een asynchrone variant.
Een filtergroep als CSP-proof Alpine-component:
<div x-data="filterGroup" class="border-b border-gray-200">
<button type="button" @click="toggle" :aria-expanded="isOpen"
class="flex w-full items-center justify-between py-3 text-sm font-medium">
<span>Merk</span>
<svg :class="iconClass" class="h-4 w-4 transition-transform" aria-hidden="true" viewBox="0 0 20 20" fill="currentColor">
<path d="M5.3 7.3a1 1 0 0 1 1.4 0L10 10.6l3.3-3.3a1 1 0 1 1 1.4 1.4l-4 4a1 1 0 0 1-1.4 0l-4-4a1 1 0 0 1 0-1.4z"/>
</svg>
</button>
<div x-show="isOpen" class="space-y-2 pb-4">
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input type="checkbox" value="acme" @change="apply" data-filter="brand" class="rounded border-gray-300 text-primary">
<span>Acme</span>
<span class="text-gray-400">(12)</span>
</label>
</div>
</div>
<script>
function filterGroup() {
return {
open: true,
isOpen() { return this.open; },
iconClass() { return this.open ? 'rotate-180' : ''; },
toggle() { this.open = !this.open; },
apply(event) {
const url = new URL(window.location.href);
const key = event.target.dataset.filter;
event.target.checked
? url.searchParams.set(key, event.target.value)
: url.searchParams.delete(key);
window.location.href = url.toString();
},
};
}
window.addEventListener('alpine:init', () => Alpine.data('filterGroup', filterGroup), {once: true});
</script>
<?php $hyvaCsp->registerInlineScript() ?>
De opties rendert PHP server-side uit de layered navigation; Alpine regelt alleen open/dicht en de navigatie. Leesbaar, testbaar en zonder dependency buiten Alpine. Dat is ook een business-argument: het onderhoud vraagt geen specialistische kennis.
Op mobiel zet je dezelfde filtergroepen in een drawer. De drawer-logica kun je overnemen van het Minicart-component in Hyvä UI, dat als drawer of popover beschikbaar is.
Productgalerij op de PDP
De galerij bepaalt voor een groot deel hoe betrouwbaar een product overkomt. Trage beelden, een kapotte zoom of geen swipe op mobiel kosten direct omzet.
Hyvä UI heeft vier galerij-varianten: fashion, fancy, grid en een variant op basis van de Splide-slider. Kies de variant die het dichtst bij je ontwerp zit. Moet je toch zelf bouwen, houd dan deze lijst aan:
- Thumbnail-navigatie via Alpine-state
- Zoom via CSS
transform, geen zware zoom-library - Swipe op mobiel via
touchstart- entouchend-events, of native CSS scroll snapping - Lazy loading voor thumbnails buiten beeld, maar nooit voor de hoofdafbeelding
Geen jQuery-carousel, geen Slick, geen RequireJS-dependency.
Checkout-UI: Hyvä UI, Hyvä Checkout of allebei?
De checkout is de laatste stap in de funnel en de plek waar een trage of rommelige interface het hardst pijn doet. Belangrijk om scherp te hebben wat welk product doet:
- Hyvä Theme is de frontend van de winkel, open source sinds november 2025.
- Hyvä Checkout is een aparte, commerciële checkout-module voor Magento 2, gebouwd op Magewire. Die regelt stappen, betaal- en verzendmethoden, adresvalidatie en de koppeling met Magento.
- Hyvä UI levert componenten rond de checkout, zoals de Minicart en de Order-confirmation-pagina. Het is geen checkout.
- Hyvä Commerce bundelt Theme, Checkout en UI.
Hyvä Checkout en Hyvä UI vullen elkaar aan: Checkout regelt de logica, de UI-conventies (forms, buttons, tokens) zorgen dat de checkout visueel aansluit op de rest van de winkel. Wie geen Hyvä Checkout gebruikt, draait in een Hyvä-winkel meestal een Luma-checkout als fallback of een checkout van een derde partij. Dan heeft Hyvä UI daar weinig invloed.
Een checkout-sectie in Hyvä UI-stijl
Velden in de checkout verdienen dezelfde discipline als de rest: zichtbare labels, de juiste autocomplete-waarden en duidelijke foutmeldingen.
<fieldset class="mb-4 space-y-3 rounded-lg border p-4">
<legend class="px-1 font-semibold text-gray-800">Bezorgadres</legend>
<div class="flex flex-col gap-1">
<label for="street" class="text-sm font-medium text-gray-700">Straat en huisnummer</label>
<input id="street" name="street" type="text" autocomplete="address-line1" required
class="form-input rounded-md border-gray-300 text-sm focus:ring-2 focus:ring-primary">
</div>
<div class="grid grid-cols-2 gap-3">
<div class="flex flex-col gap-1">
<label for="postcode" class="text-sm font-medium text-gray-700">Postcode</label>
<input id="postcode" name="postcode" type="text" autocomplete="postal-code" inputmode="text" required
class="form-input rounded-md border-gray-300 text-sm focus:ring-2 focus:ring-primary">
</div>
<div class="flex flex-col gap-1">
<label for="city" class="text-sm font-medium text-gray-700">Plaats</label>
<input id="city" name="city" type="text" autocomplete="address-level2" required
class="form-input rounded-md border-gray-300 text-sm focus:ring-2 focus:ring-primary">
</div>
</div>
</fieldset>
In Hyvä Checkout pas je dit soort markup aan via de templates en formulier-API van de module zelf, niet door losse formulieren erin te plakken. De styling volgt dezelfde tokens als de rest van je theme.
Let bij een upgrade naar Hyvä 1.4 of nieuwer op Tailwind v4: bij de introductie daarvan was Hyvä Checkout nog niet Tailwind 4-ready en moest die uit de Tailwind-compilatie van je theme worden gehouden. Controleer in de release notes van jouw Checkout-versie hoe dat nu zit.
Stappenplan voor een checkout-refactor
- Licenties checken. Heb je Hyvä Checkout en Hyvä UI al, los of via Commerce?
- Figma bijwerken. De designer gebruikt de Hyvä UI-componenten als basis voor de checkout-schermen.
- Tokens op orde. Kleuren, spacing en form-stijlen in je theme-config, zodat de checkout automatisch meekleurt.
- Templates inventariseren. Welke Checkout-templates overschrijf je en welke laat je standaard?
- Per sectie aanpassen. Eerst adres, dan verzending, dan betaling.
- Mobiel eerst testen. Daar zit het grootste deel van het verkeer en de meeste frictie.
- Meten. Vergelijk checkout-conversie en foutmeldingen vóór en na, liefst met echte gebruikersdata.
Integratie in een bestaand Hyvä-project
Draai je al op Hyvä, dan voeg je Hyvä UI-componenten gefaseerd toe:
- Hyvä UI ophalen via Composer als dev-dependency (
composer require --dev hyva-themes/hyva-ui) of via het Hyvä Portal - Per component de README lezen en alleen de templates kopiëren die je gebruikt
- Bestaande templates naast de Hyvä UI-varianten leggen en bepalen wat je vervangt
- Product cards, filters en galerij één voor één vervangen
- Per component meten: LCP, CLS en INP
Kopieer nooit de hele bibliotheek in je theme of Tailwind-scan; dat maakt je CSS onnodig groot. Een gefaseerde aanpak werkt beter dan een big-bang-rewrite: je hebt meetpunten tussendoor en kunt terug als een component zich vreemd gedraagt.
Wanneer je beter niet zelf bouwt in Alpine
- Zware interactieve applicaties. Een configurator met honderden state-combinaties wordt onhandelbaar in Alpine. Kijk dan naar een geïsoleerd eiland met een framework dat daarvoor gemaakt is, of los meer server-side op.
- Het team kent Tailwind en Alpine niet. Dan kost elk component meer tijd dan het oplevert. Investeer eerst in kennis.
- Je draait nog op Luma. Hyvä UI is geen drop-in voor Luma-widgets. Een migratie naar een Hyvä theme is dan de eerste stap.
- Het component bestaat al in Hyvä UI. Uitbreiden is bijna altijd goedkoper dan opnieuw bouwen.
De grootste valkuil die wij zien: Hyvä UI kopen, de componenten kopiëren en daarna toch JavaScript-zware oplossingen bouwen voor interactie die Alpine prima aankan. De winst zit in de discipline.
Checklist: component klaar voor productie?
- [ ] Classes gebruiken tokens uit de theme-config, geen hard-coded kleuren
- [ ] Classnamen staan voluit in de template, zodat Tailwind ze vindt
- [ ] Het Alpine-component is geregistreerd met
Alpine.data()en werkt in de CSP-build - [ ] Inline scripts worden afgesloten met
$hyvaCsp->registerInlineScript() - [ ]
init()endestroy()ruimen timers en listeners op - [ ] Alle output is ge-escaped met
$escaper - [ ] Toetsenbord en screenreader werken: focus-states,
aria--attributen,role - [ ] Varianten lopen via classes, niet via JavaScript-stijlmanipulatie
- [ ] Geen externe JavaScript-libraries tenzij het echt niet anders kan
- [ ] Afbeeldingen hebben
widthenheight; boven de vouw niet lazy - [ ] LCP, CLS en INP gemeten na integratie
Wat levert dit op?
Een eigen component dat je zo bouwt, past visueel in het design system, voegt nauwelijks JavaScript toe en is te onderhouden door iedereen die Tailwind en Alpine kent. Er zit geen eigen framework-magie in, alleen HTML, CSS, een beetje JavaScript en PHP waar de data vandaan komt. Dat is precies waarom een Hyvä-codebase na een paar jaar nog uitbreidbaar is.
Twijfel je of een eigen component de juiste aanpak is, of wil je dat iemand meekijkt naar je PLP, PDP of checkout? Via /contact plan je een gratis Tech Check in. Geen verkooppraatje, gewoon een eerlijk gesprek over wat wel en niet werkt voor jouw winkel.




