# Magento2 Hyva Dev

> |

- **Type:** Skill
- **Install:** `agentstack add skill-ddtcorex-dev-skills-hub-magento2-hyva-dev`
- **Verified:** Pending review
- **Seller:** [ddtcorex](https://agentstack.voostack.com/s/ddtcorex)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ddtcorex](https://github.com/ddtcorex)
- **Source:** https://github.com/ddtcorex/dev-skills-hub/tree/master/skills/magento2-hyva-dev

## Install

```sh
agentstack add skill-ddtcorex-dev-skills-hub-magento2-hyva-dev
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Magento 2 Hyvä Developer

Hyvä is a modern Magento 2 frontend framework with dramatically simplified JavaScript and CSS. This skill covers Hyvä-specific patterns.

## Related Skills

**REQUIRED BACKGROUND:** Load `magento2-dev-core` first — it defines the PHP/backend patterns (DI, escaping, repositories) this skill assumes for any ViewModel or backend code behind a Hyvä template.

Hyvä and Luma (`magento2-frontend-dev`) are mutually exclusive theme stacks — check the theme's `theme.xml` parent (`Hyva/default`/`Hyva/reset` vs `Magento/blank`) and `composer.json` for `hyva-themes/*` packages before assuming either applies. Pair with `govard-magento` for the container/CLI side.

## Detect the project's actual setup first

Hyvä/Tailwind conventions vary a lot by project age — check before applying a pattern:

- **Tailwind version**: v4 uses CSS-based config and `hyva.config.json` design tokens; v2/v3 use `tailwind.config.js`. Check `web/tailwind/package.json`.
- **CSP build**: `Hyva/default-csp` vs the plain `Hyva/default`/`Hyva/reset` parent in `theme.xml`. Applying CSP-only nonce patterns to a non-CSP theme (or vice versa) wastes effort.
- **Parent theme**: `Hyva/reset` (built from scratch) vs `Hyva/default` (full starter) changes how much markup/CSS already exists to extend rather than rewrite.

## Hyvä vs Luma Comparison

| Aspect | Luma | Hyvä |
|--------|------|------|
| JavaScript | ~200 resources (RequireJS/Knockout) | 2 resources (Alpine.js) |
| CSS | LESS-based | Tailwind CSS |
| Bundle Size | 500KB+ | //web
cp -r vendor/hyva-themes/magento2-default-theme/web/* app/design/frontend///web/
# For a CSP theme, copy from magento2-default-theme-csp instead
```

Then add `registration.php`, `theme.xml` (parent: `Hyva/default`, `Hyva/reset`, or `Hyva/default-csp`), and `composer.json`, install Tailwind deps and build, then `bin/magento setup:upgrade && bin/magento cache:flush` to pick up the new theme.

```
app/design/frontend/Vendor/Theme/
├── registration.php
├── theme.xml
├── composer.json
├── package.json
├── tailwind.config.js
├── package.json
├── web/
│   ├── tailwind/
│   │   ├── base/           # Preflight, resets
│   │   ├── components/     # Reusable components
│   │   │   ├── buttons.css
│   │   │   ├── forms.css
│   │   │   └── messages.css
│   │   ├── utilities/     # Custom utilities
│   │   └── theme/         # Page-specific
│   └── js/
│       └── alpinejs/      # Alpine components
├── layout/
│   └── default.xml
└── templates/
    └── ...
```

## CSP (Content Security Policy) Compliance

### Critical: PCI-DSS 4.0 (Required since April 2025)

Payment pages MUST NOT use:
- `unsafe-eval` CSP directive
- `unsafe-inline` CSP directive

### CSP Nonce Registration

**Every inline script MUST register with CSP:**

```php

registerInlineScript() ?>
">
    // CSP-compliant code

```

### CSP-Compatible Alpine.js Patterns

**WRONG (CSP violations):**
```html

Add
Ready

```

**CORRECT (CSP-compliant):**
```html

Add
Ready

```

```javascript
function initComponent() {
    return {
        count: 0,
        loading: true,
        name: '',

        increment() {
            this.count++;
        },

        isNotLoading() {
            return !this.loading;
        },

        updateName(event) {
            this.name = event.target.value;
        }
    }
}
window.addEventListener('alpine:init', () => {
    Alpine.data('initComponent', initComponent);
}, {once: true})
```

### Registering Alpine Components

```php

function initProductSlider() {
    return {
        products: [],
        currentIndex: 0,

        init() {
            // Initialization
        },

        next() {
            this.currentIndex = (this.currentIndex + 1) % this.products.length;
        },

        prev() {
            this.currentIndex = (this.currentIndex - 1 + this.products.length) % this.products.length;
        }
    }
}
window.addEventListener('alpine:init', () => Alpine.data('initProductSlider', initProductSlider), {once: true})

registerInlineScript() ?>
```

## Alpine.js Component Structure

### Basic Component

```javascript
// web/js/alpinejs/Example.js
function initExample() {
    return {
        // Observable state
        isOpen: false,
        items: [],
        selectedId: null,

        // Computed (reactive)
        get hasItems() {
            return this.items.length > 0;
        },

        // Methods
        toggle() {
            this.isOpen = !this.isOpen;
        },

        select(id) {
            this.selectedId = id;
        },

        // Lifecycle
        init() {
            // Called when component initializes
            this.loadData();
        },

        loadData() {
            fetch('/api/data')
                .then(res => res.json())
                .then(data => this.items = data);
        }
    }
}
window.addEventListener('alpine:init', () => Alpine.data('initExample', initExample), {once: true})
```

### Template Usage

```html

    Toggle

    
        
            
                
            
        
    

    function initExample() {
        // ... component logic
    }
    window.addEventListener('alpine:init', () => Alpine.data('initExample', initExample), {once: true})

registerInlineScript() ?>
```

### Passing Data from PHP

```php

escapeHtmlAttr($productsJson) ?>">

function initProductList() {
    return {
        products: [],

        init() {
            this.products = JSON.parse(this.$root.dataset.products || '[]');
        }
    }
}
window.addEventListener('alpine:init', () => Alpine.data('initProductList', initProductList), {once: true})

registerInlineScript() ?>
```

## Hyvä Utilities

Hyvä provides global utilities via the `hyva` object:

### Form Handling
```javascript
// Get form key
hyva.getFormKey()

// Submit form via POST
hyva.postForm({
    action: '/checkout',
    data: { product_id: 123, qty: 1 }
})

// Alternative with fetch
async function submitForm(url, data) {
    const formKey = hyva.getFormKey();
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify({ ...data, form_key: formKey })
    });
    return response.json();
}
```

### Cookies
```javascript
hyva.getCookie('customer_segment')
hyva.setCookie('recent_viewed', productId, 30)
```

### Formatting
```javascript
hyva.formatPrice(price, showSign)
hyva.str('Hello {0}', name)
hyva.safeParseNumber(value)
```

### DOM Manipulation
```javascript
hyva.replaceDomElement('#target', 'New content')
hyva.trapFocus(modalElement)
```

### Events
```javascript
// After Alpine initialization
hyva.alpineInitialized(function() {
    console.log('Alpine ready');
})
```

## View Models

Prefer view models (`Hyva\Theme\Model\ViewModelInterface` / Magento's `ArgumentInterface`) over blocks for passing data to templates — they keep PHP logic out of the theme directory (which should hold only templates, layout, `i18n`, and `web/` assets) and in a proper `app/code` module where Magento's DI can autoload the class.

```php
// app/code/Vendor/Module/ViewModel/ProductInfo.php
declare(strict_types=1);

namespace Vendor\Module\ViewModel;

use Hyva\Theme\Model\ViewModelInterface;
use Magento\Framework\View\LayoutInterface;

class ProductInfo implements ViewModelInterface
{
    public function __construct(
        private readonly LayoutInterface $layout
    ) {}

    public function isInStock(): bool
    {
        $product = $this->layout->getBlock('product.info')->getProduct();
        return $product && $product->isInStock();
    }
}
```

```xml

    
        Vendor\Module\ViewModel\ProductInfo
    

```

```php

isInStock()): ?>
    Add to Cart

```

## Tailwind CSS

### Tailwind v4 (CSS-based config)

Newer Hyvä themes use Tailwind v4, which drops `tailwind.config.js` for a CSS-based config plus a `hyva.config.json` design-token file — check `web/tailwind/package.json` first, the two configs are not interchangeable.

```css
/* web/tailwind/tailwind-source.css */
@import "tailwindcss";

@theme {
    --color-primary: oklch(46% 0.2 265);
    --spacing-xs: 0.5rem;
}

@layer components {
    .btn-primary {
        @apply bg-primary text-white px-4 py-2 rounded;
    }
}
```

```json
// hyva.config.json
{
  "tokens": {
    "src": "hyva.design.tokens.json",
    "format": "default",
    "cssSelector": "@theme"
  }
}
```

Generate tokens/sources with `npx hyva-sources` / `npx hyva-tokens` rather than hand-rolling them.

### Directory Structure
```
web/tailwind/
├── base/
│   └── _styles.pcss         # Preflight, typography
├── components/
│   ├── _buttons.pcss
│   ├── _forms.pcss
│   └── _messages.pcss
├── utilities/
│   └── _custom-utilities.pcss
├── theme/
│   ├── _header.pcss
│   └── _footer.pcss
└── app.css                  # Main entry
```

### Build Commands
```bash
# Development with watch
npm run watch

# Production build
npm run build

# PurgeCSS config (auto-included)
# Tailwind automatically removes unused classes
```

### Common Classes

```html

    Add to Cart

    

    

```

### Responsive Design
```html

    

```

## Layout XML

### Hyvä-Specific Handles

```xml

    

```

### Override Template
```xml

```

## Migration from Luma

### Step 1: Analyze Dependencies
```bash
# List jQuery dependencies
grep -r "require.*jquery" app/design/frontend/Vendor/Theme/web/js/

# Check Knockout bindings
grep -r "data-bind=" app/design/frontend/Vendor/Theme/templates/
```

### Step 2: Replace JavaScript
```javascript
// Luma Knockout
define(['ko'], function(ko) {
    return {
        items: ko.observableArray([]),
        addItem: function(item) {
            this.items.push(item);
        }
    };
});

// Hyvä Alpine
function initComponent() {
    return {
        items: [],
        addItem(item) {
            this.items.push(item);
        }
    }
}
```

### Step 3: Replace LESS with Tailwind
```less
// Luma LESS
.product-card {
    .lib-card();
    .lib-respond-to(@mobile, { width: 100%; });
}

// Hyvä Tailwind

```

### Step 4: Update Templates
```php
// Luma (Knockout)

// Hyvä (Alpine)

```

## Third-Party Compatibility Modules

A Luma-built third-party extension needs a Hyvä compatibility module to override its templates and JS — check the vendor's GitHub for an existing one (many ship under `hyva-themes/*`) before writing your own.

To build one: create a module that requires the original module, copy only the `.phtml` templates you need to override, replace any jQuery/Knockout JS with CSP-compatible Alpine, and sequence it after both the original module and `Hyva_Theme` in `module.xml`. Then register it so Hyvä actually picks it up:

```xml

    
        
            
                Vendor_Module
                Vendor_ModuleHyva
            
        
    

```

Without this `CompatModuleRegistry` registration, Hyvä has no way to know the compat module should override the original's frontend output — the templates get copied but never actually take effect.

## Hyvä UI & CMS Components

- **UI components** (`hyva-themes/hyva-ui`): prebuilt, template-based components installed into a theme — copy `src/*` into the theme, merge any layout XML, and add config to `etc/view.xml`.
- **CMS components**: custom Hyvä CMS blocks live in a module depending on `Hyva_CmsBase`, declared in a `components.json` schema. Key gotchas: `children` is a root-level property (not a field type), validation lives under `attributes`, and the default-value key is `default_value`, not `default`.

## Responsive Images

Use `Hyva\Theme\ViewModel\Media::getResponsivePictureHtml()` to generate `` markup instead of hand-rolling `srcset`. Set `loading="eager" fetchpriority="high"` on the LCP image (hero/first product image) and `loading="lazy"` on everything below the fold — getting this backwards is a common, easy-to-miss LCP regression.

## Testing with Playwright

Hyvä pages scatter hidden `x-show` elements around the DOM — always scope message assertions to `#messages`, never a bare `.message.error` selector, or the test will match a hidden element and give a false pass/fail. Prefer `getByRole` / `getByLabel` / scoped `getByText` over raw CSS selectors, and use web-first assertions with a longer timeout to account for Alpine's reactive re-render delay after form submits.

## Official Hyvä AI Tools

Hyvä provides official AI skills for various assistants:

| Tool | Purpose | Install |
|------|---------|---------|
| hyva-alpine-component | CSP-compatible Alpine components | `.opencode/skills/` |
| hyva-child-theme | Theme creation | `.opencode/skills/` |
| hyva-cms-component | CMS blocks | `.opencode/skills/` |
| hyva-ui-component | UI component installation | `.opencode/skills/` |

```bash
# Install Hyvä AI tools
curl -fsSL https://raw.githubusercontent.com/hyva-themes/hyva-ai-tools/main/install.sh | sh -s opencode
```

## Verification

```bash
# Build CSS
cd web/tailwind && npm run build

# Clear cache
bin/magento cache:clean layout block_html full_page

# Static content deploy
bin/magento setup:static-content:deploy -f

# Test in browser
# Check console for CSP errors
# Test with CSP headers enabled
```

## Pitfalls recap

- Hyvä replaces Luma entirely: no RequireJS, Knockout, UI-component JS, jQuery, or LESS in Hyvä templates.
- Every inline `` needs `$hyvaCsp->registerInlineScript()`, or it silently fails under CSP.
- Prefer the CSP-safe Alpine pattern (named `Alpine.data()` functions, no inline expression logic like `@click="count++"`) even on a non-CSP build — it's the cleaner default and avoids a rewrite if the project later enables CSP.
- Rebuild Tailwind (`npm run build`) after any style change, or the new classes won't be in the compiled CSS.
- Always escape output in `.phtml` with `$escaper`, same as any other Magento template.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ddtcorex](https://github.com/ddtcorex)
- **Source:** [ddtcorex/dev-skills-hub](https://github.com/ddtcorex/dev-skills-hub)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-ddtcorex-dev-skills-hub-magento2-hyva-dev
- Seller: https://agentstack.voostack.com/s/ddtcorex
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
