Hyvä
A Magento theme that keeps layout XML and view models but swaps RequireJS, Knockout, jQuery and LESS for Alpine.js and Tailwind.
$viewModels->require(\Vendor\Module\ViewModel\Example::class)Hyvä is a frontend theme for Magento 2. A theme is the set of templates, layout XML and assets that render the storefront. Hyvä keeps all of Magento's server side — modules, layout XML, blocks, view models and .phtml templates — and replaces only the browser stack that the default Luma theme ships.
What it drops
Luma loads RequireJS (a JavaScript module loader), Knockout.js (the data-binding library behind UI components), jQuery and compiled LESS. Hyvä removes all four. In their place it uses Tailwind CSS, compiled by a Node build that lives inside the theme, and Alpine.js, a small library driven by attributes in the markup. Far less JavaScript reaches the browser, and that is why teams adopt it.
How you work in it
You install it with Composer, run bin/magento setup:upgrade and bin/magento cache:flush, then apply the theme under Content > Design > Configuration. Your own theme is a child theme with <parent>Hyva/default</parent>, and template overrides work as in any theme. In a Hyvä template a $viewModels variable is always available, so a view model is fetched with $viewModels->require(MyViewModel::class) rather than declared in layout XML. $escaper behaves as it does everywhere else.
What trips people up
Third-party modules ship Knockout and jQuery frontends, and those do not run under Hyvä. Each one needs a compatibility module or a rewritten template, and that porting work is the real cost of the move. Magento's own checkout is a Knockout application; Hyvä does not rewrite it. Hyvä Checkout, built on Magewire, is a separate commercial product. The theme itself is open source under OSL and AFL.
Example
<?php
// app/design/frontend/Acme/storefront/Magento_Catalog/templates/product/view/delivery.phtml
declare(strict_types=1);
use Acme\Storefront\ViewModel\DeliveryEstimate;
use Hyva\Theme\Model\ViewModelRegistry;
use Magento\Catalog\Block\Product\View;
use Magento\Framework\Escaper;
/** @var View $block */
/** @var Escaper $escaper */
/** @var ViewModelRegistry $viewModels */
$product = $block->getProduct();
$delivery = $viewModels->require(DeliveryEstimate::class);
<div x-data="{ open: false }">
<button type="button" x-on:click="open = !open">
echo $escaper->escapeHtml(__('Delivery estimate'))
</button>
<p x-show="open" x-cloak>
$escaper->escapeHtml($delivery->forSku((string) $product->getSku()))
</p>
</div>
// x-on:click and @click are the same Alpine directive; @click is the shorthand When to use it
- Cut the JavaScript on a category page without touching any module's PHP.
- Work out whether a third-party review module needs a Hyvä compatibility module or a rewritten template.
- Rebuild a product page accordion in Alpine after moving a store from Luma.
- Add a server-computed delivery date to a product page in a Hyvä child theme.
More examples
Child theme and the layout that attaches the template
The theme also needs a registration.php that registers it as ComponentRegistrar::THEME, and a web/tailwind directory copied from the parent theme. The block class is given explicitly because the template calls getProduct(); a block declared with no class is a plain Template and has no such method. After adding either file run bin/magento cache:flush, and bin/magento setup:static-content:deploy in production mode.
<!-- app/design/frontend/Acme/storefront/theme.xml -->
<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
<title>Acme Storefront</title>
<parent>Hyva/default</parent>
</theme>
<!-- app/design/frontend/Acme/storefront/Magento_Catalog/layout/catalog_product_view.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="product.info.main">
<block class="Magento\Catalog\Block\Product\View"
name="acme.delivery"
template="Magento_Catalog::product/view/delivery.phtml"/>
</referenceContainer>
</body>
</page>The view model the template asks for
Any class implementing ArgumentInterface can be required by a Hyvä template, with no XML entry at all. The promoted constructor parameter is shorthand: private readonly TimezoneInterface $timezone in the signature is the same as declaring the property and assigning $this->timezone = $timezone in the body. A new class needs bin/magento setup:di:compile in production mode.
<?php
// app/code/Acme/Storefront/ViewModel/DeliveryEstimate.php
declare(strict_types=1);
namespace Acme\Storefront\ViewModel;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Framework\View\Element\Block\ArgumentInterface;
class DeliveryEstimate implements ArgumentInterface
{
public function __construct(
private readonly TimezoneInterface $timezone
) {
}
public function forSku(string $sku): string
{
$days = str_starts_with($sku, 'MH') ? 2 : 5;
return $this->timezone->date()->modify("+{$days} days")->format('D j M');
}
}
// Output of forSku('MH01') on 1 June 2026: 'Wed 3 Jun'Install and build
Two build systems now apply to one storefront: Magento's static content deployment and Tailwind's. Forgetting the second is the usual reason a style change appears to do nothing. The theme is on GitHub and on Hyvä's Composer repository, which needs a free license key.
composer require hyva-themes/magento2-default-theme
bin/magento setup:upgrade
bin/magento cache:flush
# compile the child theme's CSS
cd app/design/frontend/Acme/storefront/web/tailwind
npm ci
npm run build-prod # npm run watch while developing
# production mode only
bin/magento setup:static-content:deploy -f en_US
Discussion