--- name: magento2-hyva-dev description: | This skill should be used when the user asks to "create a Hyvä theme", "set up a child theme", "build an Alpine.js component", "make this CSP-compliant", "add Tailwind CSS classes", "convert Luma to Hyvä", "migrate Knockout to Alpine", work on "Hyvä checkout" or "Hyvä React components", or handle "Tailwind configuration" or "CSP nonce registration". Provides expert Hyvä theme development for Magento 2. This is a SPECIALIZED skill for Hyvä-specific patterns. DEPENDENT on magento2-dev-core for PHP/backend patterns. compatibility: claude, codex, opencode, copilot, dsh depends: [magento2-dev-core] metadata: audience: frontend developers workflow: magento references: - https://github.com/hyva-themes/hyva-ai-tools - https://docs.hyva.io/ --- # 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. Hybrid projects (e.g. dual-stack) keep both: `app/design/frontend//hyva_*` → this skill, `app/design/frontend//luma_child` → `magento2-frontend-dev`. Pair with `govard-magento` for the container/CLI side. ## Detect the project's actual setup first > **On DSH:** call `hyva_theme_inspect {classes:[""]}` — returns `{themes[],tailwind:{major,...},hyvaPackages[]}` grounded from theme.xml + package.json + tailwind.config/hyva.config + composer.lock. If any field null, check notes and downgrade to a question. > **Otherwise:** read theme.xml parent, /web/tailwind/package.json, tailwind.config.js or tailwind-source.css+hyva.config.json, and composer.lock — verify, don't memorize table (verified 2026-08-26). 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+ | <50KB | | Core Web Vitals | Challenging | Optimized | | Learning Curve | Steep | Gentle | | Maintenance | Complex | Simple | ## Theme Structure ### Creating a Child Theme Always copy `web/` from the parent theme rather than creating it from scratch — it carries the Tailwind config and build tooling the theme needs: ```bash mkdir -p app/design/frontend///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() ?> ``` > **$hyvaCsp is ambient — never demand an assignment.** The Hyvä theme > provides the `$hyvaCsp` view model to every template: a `/** @var */` > docblock (as above) is the complete setup, no > `$viewModels->require(HyvaCsp::class)` needed. `isset($hyvaCsp)` guards > are the correct pattern for templates that must also render without > Hyvä/CSP — never flag them as dead code, and never claim `isset()` is > always false from template text alone. If CSP registration genuinely > looks unwired, check layout XML / theme wiring first (a missing call to > a mechanism the project doesn't have is "not applicable", not a finding). ### CSP-Compatible Alpine.js Patterns **WRONG (CSP violations):** ```html Ready ``` **CORRECT (CSP-compliant):** ```html 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 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
registerInlineScript() ?> ``` ### Passing Data from PHP ```php
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()): ?> ``` ## 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. Alpine gating: Tailwind v4 sources come from CSS `@source` directives and `hyva.config.json` `tailwind.include`/`exclude`; editing `hyva.config.json` on a v3 setup is a silent no-op — verify `web/tailwind/package.json` `tailwindcss` major before touching either config. ### 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
``` ### Responsive Design ```html
``` ## Reviewing Tailwind/Hyvä Diffs — Verify Before Claiming When reviewing a diff against a Hyvä theme, never assert that a utility class "is missing", "compiles to nothing", or "will be purged" without verifying against the actual project. Work through this gate: ### 1. Detect the stack first Read `/web/tailwind/package.json` for the `tailwindcss` major version. Hyvä default theme 1.4.x/1.5.x ships Tailwind v4, 1.2.x/1.3.x v3, older v2. The version decides what "exists": - **Tailwind v3**: a utility exists only if it is in the default scale OR `theme.extend.*` OR produced by a plugin/safelist entry in `tailwind.config.js`. - **Tailwind v4**: numeric spacing utilities are dynamic (`w-18`, `py-3.75` work out of the box); sources come from CSS `@source` directives and `hyva.config.json` (`tailwind.include/exclude`). Editing `hyva.config.json` on a v3 setup is a silent no-op. ### 2. Check the real config before claiming absence Grep/read the theme's actual `tailwind.config.js` for `theme.extend.*`, `safelist`, and the `content` globs before any existence claim. Three traps: - `safelist` entries survive purge scanning even when no scanned source uses them — "no usage found in markup" is NOT evidence that a class is missing. - `content` globs may exclude whole trees (stock Hyvä child themes ship with the `app/code/**` glob commented out); classes outside scan scope need safelist or arbitrary-value syntax. - CMS-content classes can live in `Hyva_CmsTailwindJit` **database-stored** CSS injected at render time — they never appear in `web/css/styles.css`, so absence from built CSS is not proof of absence. ### 3. Built CSS: only if actually present If a compiled stylesheet is available (committed, or built locally), grep it for the escaped selector as final confirmation. Review worktrees usually have NO built CSS and NO `vendor/` — do not attempt to read either; infer composer dependencies from `composer.json`/`composer.lock`. If verification is impossible from where you stand, downgrade the finding to a question ("confirm X survives the build") instead of an assertion. ### 4. Dynamic class names PHP-interpolated utilities (`class="columns-"`) are invisible to the scanner. Suggest the fix ladder: CSS-variable binding via arbitrary-value syntax, or a co-located PHP comment listing every variant (Tailwind scans comments), or safelist / `@source inline()` reserved for DB-sourced values only. ### 5. Alpine under strict CSP Directive values must be dot paths — no operators, literals, globals, or spaces. Violations fail silently (console.warn only), so static review is the primary gate. Treat vendor guidance like "`x-model` is unsupported" as recommended style: dot-path `x-model` works on shipped CSP builds, so warn rather than fail. > Idea credits: verification-gate patterns distilled from makotokimura96/hyva-skills (CC BY 4.0) and hyva-themes/hyva-ai-tools (OSL-3.0). ## Layout XML ### Hyvä-Specific Handles ```xml