e Scaffolding
Hyvä operates as a licensed Composer package. The base theme requires a one-time commercial license (~€1,000) that grants access to the private repository. Install the package and generate the configuration scaffold:
# Register private repository
composer config repositories.hyva-themes composer https://hyva-themes.gitlab.io/magento2-hyva-compat/packages.json
# Install base theme
composer require hyva-themes/magento2-default-theme
# Generate initial configuration files
bin/magento hyva:config:generate
Create a child theme to isolate customizations and preserve upgrade paths. The child theme inherits all templates, layouts, and assets from the parent, allowing you to override only what is necessary.
<!-- app/design/frontend/CustomVendor/ModernStore/theme.xml -->
<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
<title>Modern Store Frontend</title>
<parent>Hyva/default</parent>
<media>
<preview_image>media/preview.jpg</preview_image>
</media>
</theme>
Step 3: Tailwind CSS Pipeline Configuration
Hyvä replaces LESS inheritance chains with a purged Tailwind CSS pipeline. The build process scans template files for utility class usage and generates a minimal stylesheet. Configure the content paths to include both your custom templates and the parent theme:
// tailwind.config.mjs
export default {
content: [
'./src/**/*.phtml',
'./src/**/*.html',
'../../vendor/hyva-themes/magento2-default-theme/**/templates/**/*.phtml',
'../../vendor/hyva-themes/magento2-default-theme/**/layout/**/*.xml'
],
theme: {
extend: {
colors: {
brand: {
50: '#f0fdfa',
100: '#ccfbf1',
500: '#14b8a6',
700: '#0f766e',
900: '#134e4a'
}
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif']
}
}
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography')
]
}
The build command differs between development and production. Development uses watch mode for hot reloading, while production applies minification and dead-code elimination:
# Development: live reload with source maps
npx tailwindcss -i ./src/css/app.css -o ./web/css/app.css --watch --content ./src/**/*.phtml
# Production: optimized output
NODE_ENV=production npx tailwindcss -i ./src/css/app.css -o ./web/css/app.css --minify
Step 4: Alpine.js Component Architecture
Hyvä replaces KnockoutJS data-bindings with Alpine.js directives. Alpine operates as a ~15 KB gzipped library that attaches reactive state directly to DOM elements. This eliminates the need for RequireJS module definitions and reduces cognitive overhead for frontend developers.
Consider a product variant selector. Instead of a separate JavaScript module and XML layout configuration, the component lives entirely within the template:
<!-- app/design/frontend/CustomVendor/ModernStore/Magento_Catalog/templates/product/view/variant-picker.phtml -->
<div x-data="variantPicker()" class="space-y-4">
<template x-for="(option, index) in availableOptions" :key="index">
<div class="flex items-center gap-3">
<input
type="radio"
:id="`option-${index}`"
:name="option.group"
:value="option.value"
x-model="selectedOption"
@change="updatePrice(option)"
class="w-4 h-4 text-brand-500 border-gray-300 focus:ring-brand-500"
>
<label :for="`option-${index}`" class="text-sm font-medium text-gray-700" x-text="option.label"></label>
<span x-show="option.priceDelta !== 0" class="text-xs text-gray-500" x-text="formatDelta(option.priceDelta)"></span>
</div>
</template>
<div class="mt-4 p-3 bg-gray-50 rounded" x-show="selectedOption">
<p class="text-sm text-gray-600">Selected: <span x-text="selectedOption" class="font-semibold"></span></p>
<p class="text-lg font-bold text-brand-700" x-text="currentPrice"></p>
</div>
</div>
<script>
function variantPicker() {
return {
availableOptions: <?= json_encode($block->getVariantOptions()) ?>,
selectedOption: null,
basePrice: <?= (float)$block->getBasePrice() ?>,
currentPrice: null,
init() {
this.currentPrice = this.formatCurrency(this.basePrice);
},
updatePrice(option) {
const adjusted = this.basePrice + (option.priceDelta || 0);
this.currentPrice = this.formatCurrency(adjusted);
},
formatDelta(delta) {
return delta > 0 ? `+${this.formatCurrency(delta)}` : '';
},
formatCurrency(value) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value);
}
}
}
</script>
Architecture Rationale:
- Alpine.js over KnockoutJS: KnockoutJS requires a heavy runtime to observe and update DOM nodes. Alpine uses declarative attributes (
x-data, x-model, @change) that compile to vanilla JavaScript at runtime. The ~15 KB footprint eliminates the parsing delay that blocks the main thread.
- Tailwind over LESS: LESS compiles to verbose CSS with deep inheritance chains that are difficult to purge. Tailwind scans templates and generates only the utility classes actually used. The resulting payload is typically 10–30 KB, drastically reducing network transfer time.
- Child Theme Inheritance: Overriding only modified templates preserves the parent theme's upgrade path. Security patches and performance improvements in the base Hyvä package apply automatically without merge conflicts.
Pitfall Guide
1. Ignoring Extension Compatibility Mapping
Explanation: Migrating without auditing third-party modules leads to broken UI components, missing checkout fields, or non-functional payment gateways. KnockoutJS/RequireJS components will not render in a Hyvä environment.
Fix: Run a full module inventory before scaffolding. Prioritize vendors with native Hyvä support. Use the compatibility layer only as a temporary bridge, not a permanent solution.
2. Over-Engineering State with Alpine.js
Explanation: Developers accustomed to React or Vue may attempt to manage complex application state within Alpine components, leading to tangled x-data objects and performance degradation.
Fix: Reserve Alpine for view-level interactions (toggles, form bindings, simple calculations). Offload complex state management to a dedicated frontend framework or server-side rendering where possible. Keep components focused on a single responsibility.
3. Misconfiguring Tailwind Content Paths
Explanation: If the content array in tailwind.config.mjs does not include all template directories, unused classes will be purged incorrectly, causing missing styles in production.
Fix: Explicitly list all custom template paths, parent theme paths, and layout XML files. Run a production build locally and visually verify critical pages before deploying. Use npx tailwindcss --debug to inspect the purge log.
4. Neglecting Layout Shift (CLS) Mitigation
Explanation: Hyvä's server-rendered HTML eliminates many CLS issues, but dynamic blocks (recommendations, ads, lazy-loaded images) can still cause layout shifts if dimensions are not reserved.
Fix: Always specify explicit width and height attributes on images and media elements. Use Tailwind's aspect-ratio utilities for responsive containers. Implement placeholder skeletons for dynamically loaded content.
5. Relying on the Compatibility Layer Long-Term
Explanation: The Hyvä compatibility layer renders Luma components inside iframes. While functional, it introduces cross-origin communication overhead, breaks SEO indexing for embedded content, and complicates analytics tracking.
Fix: Treat the compatibility layer as a migration bridge. Schedule refactoring of critical components to native Alpine/Tailwind implementations within the first 90 days post-launch.
6. Skipping Asset Optimization Alongside JS/CSS Reduction
Explanation: Reducing JavaScript and CSS payloads yields diminishing returns if images, fonts, and third-party scripts remain unoptimized. The performance gain is diluted by network bottlenecks elsewhere.
Fix: Implement WebP/AVIF image conversion, lazy loading for below-the-fold media, and font subsetting. Use HTTP/2 or HTTP/3 multiplexing to parallelize asset delivery. Audit third-party tags with a tag manager to prevent render-blocking execution.
Explanation: The checkout page is the most conversion-critical surface. Migrating it to Hyvä without careful state management can introduce validation errors, payment gateway timeouts, or address form regressions.
Fix: Pair Hyvä with a dedicated React checkout solution for complex stores, or implement a progressive migration strategy. Isolate checkout templates, validate all form submissions, and monitor TTFB and TBT specifically on /checkout routes.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| New storefront build | Full Hyvä greenfield implementation | Zero legacy debt, maximum CWV alignment, fastest time-to-market | Higher initial dev cost, lower long-term maintenance |
| Stable store with 10+ extensions | Progressive migration with compatibility layer | Minimizes disruption, allows incremental refactoring, preserves revenue | Moderate dev cost, temporary iframe overhead |
| High-conversion checkout focus | Hyvä base + React checkout integration | Isolates critical path, enables advanced state management, improves TBT | Higher licensing cost, requires React expertise |
| Budget-constrained maintenance | Luma optimization + caching layers | Avoids migration risk, leverages existing team skills | Ongoing CWV penalties, higher infrastructure scaling costs |
Configuration Template
// package.json (frontend build scripts)
{
"name": "modern-store-frontend",
"version": "1.0.0",
"scripts": {
"dev": "npx tailwindcss -i ./src/css/app.css -o ./web/css/app.css --watch",
"build": "NODE_ENV=production npx tailwindcss -i ./src/css/app.css -o ./web/css/app.css --minify",
"lint": "eslint src/**/*.phtml --ext .phtml",
"test": "jest --config jest.config.js"
},
"devDependencies": {
"tailwindcss": "^3.4.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"alpinejs": "^3.14.0"
}
}
<!-- app/design/frontend/CustomVendor/ModernStore/Magento_Theme/layout/default.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<css src="css/app.css" />
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js" defer="defer"/>
</head>
<body>
<referenceBlock name="head.additional">
<block class="Magento\Framework\View\Element\Template" name="custom.meta" template="Magento_Theme::html/meta.phtml"/>
</referenceBlock>
</body>
</page>
Quick Start Guide
- Initialize Repository: Run
composer require hyva-themes/magento2-default-theme and execute bin/magento hyva:config:generate to scaffold configuration files.
- Create Child Theme: Add
theme.xml in app/design/frontend/YourVendor/YourTheme/ with <parent>Hyva/default</parent> and register it in the Magento admin panel.
- Configure Tailwind: Copy the provided
tailwind.config.mjs, adjust content paths to match your template directories, and run npm run dev to start the watch process.
- Migrate First Component: Replace a simple KnockoutJS block (e.g., newsletter signup or category filter) with an Alpine.js equivalent using
x-data and @submit directives.
- Validate & Deploy: Run
npm run build to generate the production stylesheet, clear Magento caches (bin/magento cache:clean), and verify Lighthouse scores on a staging environment before pushing to production.