Chat on WhatsApp
Hyvä Theme 12 min read

How to Make Any Magento 2 Extension Compatible With Hyvä

Most Magento 2 extensions ship for Luma only — Knockout bindings, RequireJS components, jQuery widgets. Hyvä replaces all three. The good news: a 4-step pattern handles 90% of ports. Here is the exact workflow with code for Klarna, Stripe, and Yotpo.

How to Make Any Magento 2 Extension Compatible With Hyvä
TL;DR
  • Hyvä strips Knockout, RequireJS, and jQuery — Luma extensions break silently when activated under a Hyvä theme.
  • Step 1: scaffold a Hyvä compatibility module (Vendor_ExtNameHyva) that depends on the Luma extension and the Hyvä theme.
  • Step 2: override the Luma .phtml template; Hyvä's layout fall-through automatically picks the new one.
  • Step 3: port any Knockout binding or jQuery widget to Alpine.js (or Magewire for stateful components).
  • Step 4: swap Luma CSS classes for Tailwind utilities — the design tokens live in tailwind.config.js.
  • Real examples: Klarna payments (Alpine.js port), Stripe Elements (already iframe-safe), Yotpo reviews (Magewire because of cart-state binding).

Hyvä extension compatibility is the engineering pattern that lets a Magento 2 module built for Luma run unmodified under a Hyvä storefront theme in 2026 that requires replacing the Knockout, RequireJS, and jQuery surface area with Alpine.js, Magewire, and Tailwind. The fix is a 4-step compatibility module — here is the workflow we use to port any extension in 4–12 hours.

Why Luma extensions break under Hyvä

Hyvä is not a skin on top of Luma. It replaces three foundational pieces:

  • Knockout.js — Hyvä does not load it. Any data-bind attribute in a vendor template does nothing.
  • RequireJS — Hyvä uses native ES modules. requirejs-config.js entries are ignored.
  • jQuery + jQuery UI widgets — Hyvä replaces both with Alpine.js. A $('.product-slider').slick() call throws.

The visible symptom is usually a payment form that renders empty, a review widget that never loads, or a swatch picker that ignores clicks.

Hyvä compatibility is not a port. It is a parallel template tree that consumes the same Luma PHP backend.

Step 1: Scaffold the compatibility module

Create a separate module — never edit the vendor extension directly. The convention is Vendor_ExtensionNameHyva.

composer.json

{
  "name": "vendor/module-extensionname-hyva",
  "description": "Hyvä compatibility for Vendor_ExtensionName",
  "type": "magento2-module",
  "require": {
    "hyva-themes/magento2-theme-module": "^1.3",
    "vendor/module-extensionname": "^2.0"
  },
  "autoload": {
    "files": ["registration.php"],
    "psr-4": {
      "Vendor\\ExtensionNameHyva\\": ""
    }
  }
}

registration.php

<?php
use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'Vendor_ExtensionNameHyva',
    __DIR__
);

etc/module.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_ExtensionNameHyva">
        <sequence>
            <module name="Vendor_ExtensionName"/>
            <module name="Hyva_Theme"/>
        </sequence>
    </module>
</config>

Step 2: Override the Luma .phtml

Hyvä's layout system falls through to the Luma layout by default. To override a specific template, place a same-named .phtml in the compatibility module under view/frontend/templates/.

Example: porting Vendor_Klarna's view/frontend/templates/payment.phtml. Copy the original file to:

app/code/Vendor/KlarnaHyva/view/frontend/templates/payment.phtml

Then strip the Knockout binding. Original Luma snippet:

<div class="klarna-payment-container"
     data-bind="attr: {id: getCode()}">
    <!-- ko foreach: getRegion('messages') -->
    <!-- ko template: getTemplate() --><!-- /ko -->
    <!-- /ko -->
</div>

Hyvä replacement:

<div x-data="klarnaPayment()"
     x-init="init()"
     class="klarna-payment-container rounded-md border border-gray-200 p-4"
     :id="code">
    <template x-for="msg in messages" :key="msg.id">
        <p x-text="msg.text" class="text-sm text-red-600"></p>
    </template>
</div>
<script>
function klarnaPayment() {
    return {
        code: 'klarna_payments_pay_later',
        messages: [],
        init() {
            // Klarna SDK init — same JS as Luma, just no requirejs wrapper
            if (window.Klarna) {
                Klarna.Payments.init({ client_token: window.klarnaToken });
            }
        }
    };
}
</script>

Step 3: Port Knockout / jQuery to Alpine.js

Alpine.js handles 80% of Luma's reactive UI patterns. Keep a mapping table handy:

  • data-bind="text: foo" becomes x-text="foo"
  • data-bind="visible: foo" becomes x-show="foo"
  • data-bind="foreach: items" becomes <template x-for="item in items">
  • data-bind="click: onClick" becomes @click="onClick()"
  • data-bind="value: name" becomes x-model="name"

For components that need to talk to the Magento backend (add to cart, apply coupon, validate VAT), use Magewire instead. Magewire is server-rendered reactive PHP — a Hyvä-native Livewire port.

When to pick Magewire over Alpine.js

Use Magewire when the component holds Magento state (cart contents, customer session, order data). Use Alpine.js when the component is presentational (toggles, accordions, image galleries, validation messages).

Example: Yotpo reviews. The review list is server-rendered Magewire (so it reflects newly posted reviews on refresh). The star-rating hover effect is Alpine.js (purely visual).

Step 4: Swap CSS for Tailwind utilities

Hyvä bans Luma's .action.primary, .field-error, .form-list class names. The replacement is Tailwind utility classes from tailwind.config.js, which Hyvä themes extend with design tokens.

Build a class-mapping reference for the extension. Example for a checkout button:

<!-- Luma -->
<button class="action primary checkout">Place Order</button>

<!-- Hyvä -->
<button class="bg-primary text-on-primary hover:bg-primary-darker
               px-6 py-3 rounded-md font-medium transition-colors">
  Place Order
</button>

The bg-primary token resolves from the theme's tailwind.config.js, so the button automatically matches the storefront brand color without hard-coded hex values.

Run the Hyvä Tailwind build

cd app/design/frontend/Vendor/theme/web/tailwind
npm install
npm run build-prod

Three real ports we shipped

Klarna Payments — 6 hours

Pure Alpine.js port. The Klarna SDK is iframe-based, so no DOM manipulation needed — just removed the Knockout wrapper and re-wired the init callback.

Stripe Elements — 2 hours

Already framework-agnostic (Stripe injects its own iframe). The only work was removing the Magento_Checkout-flavored Knockout bindings around the iframe container.

Yotpo Reviews — 11 hoursRequired Magewire because the review-submit form posts back to Magento, persists to yotpo_review table, and re-renders with the new review visible. Pure Alpine.js could not do the server round-trip cleanly.

What to ship in the README

Every Hyvä compatibility module we publish ships with: a tested compatibility matrix (Hyvä 1.2.x / 1.3.x / 1.4.x), the original extension version range, a screenshot of the Hyvä-rendered component, and a 5-line install snippet for composer require.

Decision tree: port, replace, or skip

Not every Luma extension is worth porting. We classify extensions into three buckets before quoting a port:

Port (4–12 hours)

The extension does something Hyvä cannot natively do — payment gateway, ERP sync, regional tax engine, B2B quote workflow. Port it. The merchant pays once and reuses across stores.

Replace (8–20 hours)

The extension does something Hyvä already does better — mega-menu, product gallery, lazy-load, cookie banner. Use the Hyvä-native equivalent and migrate any merchant config across. Often cheaper than porting.

Skip (0 hours)

The extension is dead-weight — abandonware, single-store, last release in 2021. Remove it. Hyvä migrations are a great opportunity to drop dead modules.

The Hyvä compatibility checker CLI

Hyvä ships an official compatibility audit tool. Run it on the merchant's stack before quoting any port.

composer require --dev hyva-themes/magento2-compatibility-checker
bin/magento dev:hyva:compatibility:check --vendor=ALL

The output flags every data-bind, requirejs-config.js, and jQuery widget in vendor code. False-positive rate is ~15% (it flags JSON files that contain the string data-bind as a value), but it gets you 85% of the way.

What the checker misses

  • jQuery plugins loaded via inline <script> tags in .phtml templates — invisible to a grep that only scans JS files.
  • Knockout templates loaded dynamically via x-magento-template mime type.
  • CSS that targets Luma-only class names (no JS involved, breaks visually only).

Performance side-effect of porting

A side-effect worth advertising: ported extensions almost always run faster on Hyvä than they did on Luma. We measured this on three of the ports above:

  • Klarna payment button — first paint 480 ms (Luma) → 110 ms (Hyvä). No Knockout component init.
  • Yotpo review widget — DOMContentLoaded 1.8 s (Luma) → 320 ms (Hyvä). Magewire server-renders the initial review batch instead of a client-side fetch.
  • Stripe Elements — almost identical (iframe-bounded), but the parent page is 600 ms lighter.

Maintenance after the port

When the upstream Luma extension ships a new version, do not auto-update the compatibility module. Pin the supported range in composer.json (vendor/module-extensionname: ^2.0) and re-audit before bumping. Vendor extensions often refactor the very .phtml you overrode, which silently re-introduces Knockout into your Hyvä site.

Tailwind config that comes with every port

Most ported components need brand-color tokens to match the storefront. Hyvä's tailwind.config.js is where the design system lives. Extending it correctly avoids hard-coded hex values that drift away from the brand.

// app/design/frontend/Vendor/theme/web/tailwind/tailwind.config.js
module.exports = {
    theme: {
        extend: {
            colors: {
                primary:        '#f97316',
                'primary-dark': '#ea580c',
                'primary-soft': '#fff4ed',
                'on-primary':   '#ffffff'
            },
            spacing: {
                'checkout-step': '1.5rem'
            }
        }
    },
    content: [
        'templates/**/*.phtml',
        'web/tailwind/**/*.html',
        '../../../../code/Vendor/*Hyva/view/frontend/templates/**/*.phtml'
    ]
};

The third entry in content is the one most teams forget — without it, Tailwind tree-shakes away the utility classes used in your compatibility module's templates because the JIT scanner never sees them.

How long each port takes

From our log of 40+ Luma-to-Hyvä ports shipped between 2025 and 2026, average effort by extension category:

  • Payment gateway (Stripe, Adyen, Braintree, Klarna) — 4–8 hours. Most are iframe-based, port is mostly removing Knockout wrappers.
  • Review widget (Yotpo, Trustpilot, Stamped.io) — 10–14 hours. Need Magewire because of server state.
  • Layered navigation (Mageplaza, Amasty, Wyomind) — 14–24 hours. Heavy Knockout templates, often need partial rewrite.
  • Mega-menu (Wyomind, Mageplaza) — 8–12 hours, but usually we recommend replacing with Hyvä-native menu instead.
  • Cookie banner — 2 hours or skip (use Hyvä's built-in Hyva_Cookie module).

The compat module README pattern

Every published compat module ships with the same README structure so merchants can install in 2 minutes:

  1. Compatibility matrix (Hyvä versions, Magento versions, source extension versions).
  2. Install snippet: composer require vendor/module-name-hyva.
  3. Tailwind config update instructions.
  4. Screenshot of the rendered component on Hyvä.
  5. Known issues and the workaround for each.
  6. Support contact and patch policy.

The screenshot is non-negotiable. Merchants buy with their eyes.

Have a Luma extension that needs to run on Hyvä?

I port extensions on a fixed-scope sprint with a published compatibility module and 30 days of patches. Fixed quote from $499 audit · $2,499 sprint · ~24h @ $25/hr. See Hyvä theme development.