Skip to content

Frontend Code ​

Every Lynx theme is one Vite build. It collects the frontend sources of every enabled module, applies the theme's overrides, and produces one bundle per theme. This page covers how your module's frontend code fits into that build and how to style components so they follow each design.

Where the code lives ​

All frontend code of a module sits under one fixed folder:

view/frontend/lynx/
├── vue/
│   ├── package.json          ← optional, lists the npm packages this module needs
│   └── src/
│       ├── Components/       ← components registered in di.xml
│       ├── stores/           ← state shared between components
│       └── utils/            ← plain functions

Only src/ is required. Do not put sources in view/frontend/web/: Magento publishes everything there to pub/static.

Importing by module name ​

Any .vue or .ts file under a module's src/ can be imported from anywhere by the module name plus its path in src/. Write the extension for .vue files, leave it out for .ts:

ts
import DynamicNode from 'OOCS_LynxTheme/Components/DynamicNode.vue'
import { useMessagesStore } from 'OOCS_LynxTheme/stores/global/messagesStore'
import { config } from 'OOCS_LynxTheme/utils/storeConfig'
import { useWishlistStore } from 'Vendor_Module/stores/wishlistStore'

The module name is the one in your registration.php. These imports go through the theme's file resolution, so a theme that overrides a file gets its version everywhere, relative imports inside the module included. See Themes and Overrides.

Importing from another module makes it a hard build dependency: if that module is missing, the build fails at the import. Import only from modules listed in your <sequence> and composer.json. For an optional integration, go through a di.xml pool (components, payment renderers) or a store event bus such as analyticsStore, never a static import.

Types used only in import type can live in a .d.ts file. Types that are also used at runtime go in a regular .ts file.

Shared state and utilities ​

State shared between components lives in a state store, written with Pinia. Put a state store in the module that owns the feature (Vendor_Module/stores/wishlistStore); the bundler splits it into its own chunk, so pages without your feature do not load it.

OOCS_LynxTheme holds what every page needs. Use these rather than writing your own:

ImportWhat it gives you
OOCS_LynxTheme/stores/global/messagesStoreThe storefront messages: add('success' | 'error' | 'warning' | 'notice', text)
OOCS_LynxTheme/stores/global/customerDataStoreMagento customer data sections (cart, customer, wishlist…) and login state: section(name), cartCount, isLoggedIn, displayName; reload(sections) after a request that changed them
OOCS_LynxTheme/stores/global/formKeyStoreThe form key for POST requests: ensure()
OOCS_LynxTheme/stores/global/analyticsStoreAnalytics event bus: track(event, params), addTracker(fn)
OOCS_LynxTheme/utils/storeConfigStore configuration rendered into the page: config<T>(key, fallback), with keys such as storeCode, currencyCode, baseUrl (store code included when it is in URLs), priceFormat, labels
OOCS_LynxTheme/utils/priceformatPrice(amount) in the store's currency format

A folder named stores/global/ is bundled into the always-loaded chunk, in any module. Use that name only for a state store that every page needs; the store of a feature belongs elsewhere.

ts
// stores/noticeStore.ts
import { defineStore } from 'pinia'

export const useNoticeStore = defineStore('vendorNotice', {
    state: () => ({ dismissed: false }),
    actions: {
        dismiss(): void {
            this.dismissed = true
        },
    },
})

Components on every page ​

A component that is not a layout node but must run on every page (a global listener, a third-party SDK hook) can be mounted by Lynx next to the layout, on every page including the checkout. Add its path to the pageComponents argument of OOCS\LynxTheme\ViewModel\StoreConfig:

xml
<type name="OOCS\LynxTheme\ViewModel\StoreConfig">
    <arguments>
        <argument name="pageComponents" xsi:type="array">
            <item name="vendor_module" xsi:type="string">Vendor_Module/Components/ChatLauncher</item>
        </argument>
    </arguments>
</type>

To add it only under a condition (a feature enabled in configuration), append to pageComponents in an afterBuild plugin on the same class instead. These components load on every page, so keep them small. Merchant-placed scripts and trackers are better as headless components in the backstage zone.

npm dependencies ​

A module that needs an npm package lists it in a package.json next to its src/:

json
{
  "name": "vendor-module-vue",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "some-sdk": "4.2.0"
  }
}

Do not run npm install in the module. bin/magento lynx:theme:setup installs the packages of every enabled module into the node_modules of each Lynx theme, and your module's code imports them from there. When two modules, or a module and the theme, pin the same package to different versions, setup stops and names both. Vue, Inertia.js and Pinia come from the Lynx build kit; do not add them to a module.

What gets built ​

bin/magento lynx:theme:setup <themes> writes each theme's lynx/.lynx/package.json: the npm packages and, under lynx.modules, every enabled module that has a view/frontend/lynx/vue/src folder. The build reads that list. After you enable or disable a module, run the command again (lynx:static:deploy stops until you do): a disabled module's components, and theme overrides of it, leave the bundle.

Each component file becomes its own chunk, named after its module path (Vendor_Module/Components/Highlight.min.js), loaded only on pages that contain the component. After every build, the log lists each chunk with the source file it came from, which shows whether a theme override or the module's own file was used.

Type checking runs before each build (npm run build runs type-check first). The theme generates tsconfig.modules.json with a path entry per module, so imports of Vendor_Module/... are type-checked against the files the build uses. See Setup for the commands and the dev server.

Translations: on the server only ​

Lynx has no translation step in the browser. It does not load Magento's js-translation.json dictionary, mage/translate or $t(), and components never call a translate function. Every string the shopper sees is translated on the server, in the locale of the store view, with Magento's __() and its dictionaries (module and theme i18n/<locale>.csv, language packs), and reaches the component already translated.

That keeps the bundle free of a dictionary, makes SSR output and the browser render the same text, and lets language packs translate Lynx like any Magento module.

Where a component gets its text:

TextComes from
Labels of a component placed in PageBuilder (buttons, headings, placeholders, aria-label, messages)The provider's labels prop, see Text and labels
Text the merchant types in the editor_i18n settings, resolved for the store view by the provider with LocalizedText, see Setting Types
A component that is not a layout node (the page shell, flash messages, your own global UI)The store config: config('labels') from OOCS_LynxTheme/utils/storeConfig
A payment or shipping method rendererlabels in the method's getConfig(), see Payment Methods
A page returned by your controllerA labels prop you pass to the page, see Pages and Controllers
Flash messages, REST and validation errorsAlready translated by Magento: show the message the server returns

To add labels to the store config for a global component, write an after plugin on OOCS\LynxTheme\ViewModel\StoreConfig::build():

php
<?php
declare(strict_types=1);

namespace Vendor\Module\Plugin;

use OOCS\LynxTheme\ViewModel\StoreConfig;

class AddStoreConfigLabels
{
    /**
     * @param StoreConfig $subject
     * @param array $result
     * @return array
     */
    public function afterBuild(StoreConfig $subject, array $result): array
    {
        // Prefix keys with your module so they never collide with Lynx's own
        $result['labels']['vendorModuleDismiss'] = (string) __('Close');

        return $result;
    }
}

Rules that keep translations working:

  • Use the phrases Magento already has where they fit ("Add to Cart", "Sign In", "Items %1 to %2 of %3 total"): every language pack translates them already. Add new phrases to your module's i18n/en_US.csv.
  • Values only the browser knows stay as placeholders. Translate __('%1 items') on the server and replace %1 in the component; never build a sentence from pieces in JavaScript.
  • No English fallback in the component. A missing key renders empty, so it shows up during development instead of shipping untranslated text.
  • After changing a CSV, run bin/magento cache:clean translate full_page, plus bin/magento lynx:compile translate if the store uses compiled files.

Translations stored in the database through Inline Translation do not apply on Lynx store views; see Differences from Standard Magento.

Styling ​

Components are styled with Tailwind CSS 4. Tailwind scans your module's src/ automatically: the build writes a @source line for every module and theme override folder into the theme's lynx/dist/sources.css. Do not add @source paths that count ../ to your module; the depth differs between app/code, vendor/ and other install locations.

Write class names in full so Tailwind can find them. Do not build them from pieces ('lg:col-span-' + n); use a map:

ts
const SPAN: Record<number, string> = {
    1: 'lg:col-span-1',
    2: 'lg:col-span-2',
    3: 'lg:col-span-3',
}

Follow the design settings ​

Merchants change colors, corners, borders, widths and fonts per design in the editor's design settings. Those settings write CSS variables, and Tailwind utilities read them at runtime. A component styled with these tokens follows every design without a rebuild; a hardcoded color or radius does not.

UseClasses
Texttext-base-content, muted: text-base-content/70, text-base-content/60
Backgroundsbg-base-100 (page, cards, fields), bg-base-200 (sunken panels), bg-base-300
Bordersborder-base-300, width border-(length:--border)
Brand colorsbg-primary text-primary-content, secondary, accent, neutral, each with its -content partner
Statesinfo, success, warning, error (with -content)
Linkstext-link, hover:text-link. Keep text-primary for icons and states.
Cornersrounded-box (cards, panels), rounded-field (inputs), rounded-selector (checkbox, badges), rounded-button
Shadowsshadow-card, shadow-menu, shadow-dialog, shadow-thin (scaled by the design's Depth)
Widthsmax-w-site, max-w-content, max-w-narrow; header height min-h-header

Lay a color's text on it with its -content partner (bg-primary text-primary-content), so both stay readable when the merchant changes the palette.

Controls: daisyUI with the l- prefix ​

Buttons, fields and a few other controls use daisyUI 5 classes with the l- prefix. They carry the design's shape settings (corners, border width, control height), so use them instead of rebuilding controls with utilities:

ControlClasses
Buttonl-btn l-btn-primary; secondary: l-btn l-btn-outline l-btn-secondary; sizes l-btn-sm, l-btn-lg
Text fieldl-input, with an error: l-input-error
Select, textareal-select, l-textarea
Checkbox, radiol-checkbox l-checkbox-primary, l-radio l-radio-primary
Card, tablel-card l-card-border, l-table
Loading placeholderl-skeleton
Inline noticel-alert

Only these daisyUI components are built: button, input, select, textarea, checkbox, radio, alert, card, table and skeleton. Other daisyUI classes (l-modal, l-menu, l-dropdown…) produce no CSS. Build dropdowns, drawers, accordions and similar with plain Tailwind and the tokens above.

Which design setting reaches which class ​

Each design setting writes one CSS variable on :root of the storefront. A component picks the change up only through the classes that read that variable:

Design settingCSS variableClasses that follow it
Colors › Button, Button text--color-primary, --color-primary-contentbg-primary, text-primary, border-primary, text-primary-content; l-btn-primary, l-checkbox-primary, l-radio-primary; focus rings of l-btn and fields
Colors › Secondary button, Secondary button text--color-secondary, --color-secondary-contentbg-secondary, text-secondary-content, l-btn-secondary
Colors › Link--color-linktext-link, hover:text-link
Colors › Text--color-base-contenttext-base-content and its opacities (/70, /60); the page's default text
Colors › Page background--color-base-100bg-base-100; the background of l-input, l-select, l-textarea, l-card
Colors › Surface--color-base-200bg-base-200
Colors › Border--color-base-300border-base-300, divide-base-300; the border of l-input, l-select, l-textarea
Colors › Dark surface, Dark surface text--color-neutral, --color-neutral-contentbg-neutral, text-neutral-content
Colors › Info, Success, Warning, Error (and their text)--color-info … --color-error-contentbg-success, text-error, border-warning…, l-alert, l-input-error
Typography › Body font scaleroot font-sizeevery rem size: the text-* scale, spacing and widths in rem
Typography › Heading font scale--heading-scaletext-lg to text-5xl
Layout › Site, Content, Narrow width--width-site, --width-content, --width-narrowmax-w-site, max-w-content, max-w-narrow; the frame of Group and align wide/full
Layout › Header height--spacing-headermin-h-header, h-header
Shape › Corners — boxes--radius-boxrounded-box; l-card, l-alert
Shape › Corners — buttons--radius-buttonrounded-button; l-btn
Shape › Corners — fields--radius-field (and --radius-selector)rounded-field, rounded-selector; l-input, l-select, l-textarea, l-checkbox
Shape › Border width--borderborder-(length:--border); the borders of l-btn, l-input, l-select, l-textarea, l-checkbox, l-radio
Shape › Control height--size-fieldthe height of l-btn, l-input, l-select
Shape › Small control size--size-selectorthe size of l-checkbox, l-radio
Shape › Depth--depthshadow-card, shadow-dialog

What does not follow the settings: Tailwind's fixed palette and scales, such as bg-blue-600, text-gray-500, rounded-lg, border-2, shadow-md, h-10 on a button, max-w-7xl or text-[15px]. Use them only for what must stay the same in every design (an icon's size, a spacing inside your component), never for colors, corners, borders, shadows, control sizes or page widths.

The editor's preview applies a changed setting at once, so a quick way to check a component is to open it in the preview and change Button color, Corners and Border width: everything that should follow has to change.

Font sizes ​

Use the theme's type scale: text-sm, text-base, text-lg up to text-5xl. text-sm (14px) is the smallest size for storefront text; do not use text-xs or arbitrary sizes like text-[13px]. Sizes from text-lg up are headings and scale with the design's heading size setting.

Component width ​

A content component does not frame itself. Its root is w-full, without max-w-*, mx-auto or outer padding: the merchant places it in a Group, which sets the width and spacing, and the page frame adds the side gutter. A width that belongs to the component's own design (a form at max-w-md mx-auto) stays inside it.

Right-to-left languages ​

Lynx sets dir="rtl" on store views whose locale is written right to left (Arabic, Persian, Hebrew). Write components so they mirror without extra code:

Instead ofUse
ml-*, mr-*, pl-*, pr-*ms-*, me-*, ps-*, pe-*
left-*, right-*start-*, end-*
text-left, text-righttext-start, text-end
border-l, rounded-r-*border-s, rounded-e-*
  • Where a physical direction is unavoidable, pair the variants: ltr:-translate-x-full rtl:translate-x-full for a panel sliding in from the side.
  • Flip directional icons (chevrons, arrows) with rtl:scale-x-[-1]. Leave symmetric icons (close, plus, search) alone.
  • A scroll or slide track that you move with translateX or scrollLeft can be fixed with dir="ltr"; buttons outside it still mirror.
  • In JavaScript that depends on the horizontal direction (swipe handling), read getComputedStyle(el).direction === 'rtl' on the element, not document.dir.

To check quickly, run document.documentElement.dir = 'rtl' in the browser console.

Styles beyond components ​

Theme-wide CSS (fonts, extra tokens) goes in the theme's styles.css, see Themes and Overrides. A module can also ship plain CSS files in view/frontend/lynx/css/; a theme imports them with @import "Vendor_Module/css/file.css".

Subresource integrity on the checkout ​

On the checkout page, Lynx prints an import map with a SHA-256 hash for every JavaScript chunk, and the browser refuses a chunk whose content does not match. This needs nothing from your component, but it has consequences:

  • Serve the built files unchanged. A CDN or server module that minifies or rewrites JavaScript breaks the checkout. When static files come from another origin, it must send CORS headers.
  • The hashes are cached in Magento's block_html cache. After a new build, clean that cache (deployments do), or the checkout refuses the new chunks.
  • Keep Magento's Move JS code to the bottom of the page off for Lynx store views: the import map has to come before every module script.