Hyvä replaces Luma's LESS and Knockout.js with two tools: Tailwind CSS for styling and Alpine.js for interactivity. This guide shows how they fit together in a Hyvä 1.4 child theme, with examples you can reuse.
How Styles Are Built
Tailwind scans your templates for class names and generates only the CSS you use. In Hyvä, the build runs from the theme's web/tailwind folder and writes web/css/styles.css:
cd app/design/frontend/MageServices/hyva-child/web/tailwind
npm ci --ignore-scripts
npm run watch # during development
npm run build # before deployment
Because Tailwind only includes classes it finds, never build class names dynamically, such as "text-" . $color. Write full class names in templates or map values to complete classes in PHP.
Where to Put Custom CSS
Hyvä 1.4 organises source CSS into folders under web/tailwind:
| Folder | Use it for |
|---|---|
base/ | Hyvä's preflight and default element styles |
components/ | Reusable component styles such as buttons, forms and messages |
utilities/ | Custom utility classes |
theme/ | Page-specific and custom styles for your theme |
generated/ | Files generated by npx hyva-sources and npx hyva-tokens — do not edit by hand |
Prefer utility classes in templates. Use component CSS with Tailwind's @apply only for patterns repeated across many templates, such as buttons.
/* web/tailwind/components/product-badge.css */
.product-badge {
@apply inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wide;
}
.product-badge--sale {
@apply bg-red-100 text-red-800;
}
In Tailwind v4, theme values such as colours and fonts are defined in CSS rather than a JavaScript config file. Hyvä adds design tokens on top, which are generated into generated/hyva-tokens.css. Follow Hyvä's design token documentation for your version when changing brand colours.
Alpine.js Basics in Hyvä Templates
Alpine adds behaviour directly in markup. A component is an element with x-data; inside it you bind state and events:
<?php
/** @var \Magento\Framework\Escaper $escaper */
?>
<div x-data="{ open: false }" class="border-b border-gray-200 py-4">
<button type="button"
class="flex w-full items-center justify-between text-left font-semibold"
@click="open = !open"
:aria-expanded="open.toString()">
<span><?= $escaper->escapeHtml(__('Delivery & Returns')) ?></span>
<span x-text="open ? '−' : '+'" aria-hidden="true"></span>
</button>
<div x-show="open" x-cloak x-transition class="mt-3 text-sm text-gray-700">
<?= $escaper->escapeHtml(__('Free delivery on orders over £50. Returns accepted within 30 days.')) ?>
</div>
</div>
A Reusable Component With Server Data
For anything beyond a toggle, define the component in a function and pass server data as JSON. Hyvä's convention is a global function whose name starts with init:
<?php
declare(strict_types=1);
use Magento\Framework\Escaper;
use Magento\Framework\View\Element\Template;
/** @var Template $block */
/** @var Escaper $escaper */
$config = [
'endpoint' => $block->getUrl('hello/greeting/lookup'),
'minLength' => 2,
];
?>
<div x-data="initNameLookup(<?= $escaper->escapeHtmlAttr(json_encode($config)) ?>)" class="max-w-md">
<label for="name-lookup" class="label"><?= $escaper->escapeHtml(__('Your name')) ?></label>
<input id="name-lookup" type="text" class="form-input w-full" x-model="name" @input.debounce.400ms="lookup()">
<p class="mt-2 text-sm" x-show="message" x-text="message"></p>
</div>
<script>
function initNameLookup(config) {
return {
name: '',
message: '',
async lookup() {
if (this.name.length < config.minLength) {
this.message = '';
return;
}
const url = new URL(config.endpoint, window.location.origin);
url.searchParams.set('name', this.name);
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
const data = await response.json();
this.message = data.message || data.error || '';
}
};
}
</script>
Note that Alpine's x-text sets text content, so values from the server are not interpreted as HTML. Avoid x-html with any data that users can influence.
If you use the CSP-compatible Hyvä theme, components must be registered in a CSP-safe way rather than with inline expressions. Follow Hyvä's CSP guidelines for your theme version.
Communicating Between Components
Hyvä components talk to each other with browser events. Hyvä itself dispatches events such as private-content-loaded when customer section data is available:
<div x-data="{ cartCount: 0 }"
@private-content-loaded.window="cartCount = $event.detail.data.cart?.summary_count || 0">
<span class="sr-only">Items in cart:</span>
<span x-text="cartCount"></span>
</div>
Dispatch your own events with window.dispatchEvent(new CustomEvent('my-event', { detail: { ... } })) and listen with @my-event.window.
Performance Tips
- Keep Alpine components small; render as much HTML as possible on the server.
- Use
x-cloakon elements hidden by default to avoid flashes of content. - Load third-party scripts only where needed and after user interaction where possible.
- Run
npm run buildfor production; watch builds are larger.
Frequently Asked Questions
Which Tailwind version does Hyvä use?
Hyvä 1.4 uses Tailwind CSS v4. Hyvä 1.2 and 1.3 used Tailwind v3.
Can I use jQuery in Hyvä?
You can load it, but it defeats much of Hyvä's performance benefit. Use Alpine.js and native browser APIs instead.
Why are my Tailwind classes not applied?
The class is probably not found during the build. Check the file is in a scanned path and the class name is written in full, then rebuild.
Need a Hyvä developer?
Our team builds Hyvä themes, components and compatibility modules every week.
Written by the Magento Services engineering team — Magento 2, Adobe Commerce and Hyvä specialists since 2014. We write about problems we solve on real client stores.