Appearance
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 functionsOnly 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:
| Import | What it gives you |
|---|---|
OOCS_LynxTheme/stores/global/messagesStore | The storefront messages: add('success' | 'error' | 'warning' | 'notice', text) |
OOCS_LynxTheme/stores/global/customerDataStore | Magento 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/formKeyStore | The form key for POST requests: ensure() |
OOCS_LynxTheme/stores/global/analyticsStore | Analytics event bus: track(event, params), addTracker(fn) |
OOCS_LynxTheme/utils/storeConfig | Store 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/price | formatPrice(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:
| Text | Comes 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 renderer | labels in the method's getConfig(), see Payment Methods |
| A page returned by your controller | A labels prop you pass to the page, see Pages and Controllers |
| Flash messages, REST and validation errors | Already 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%1in 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, plusbin/magento lynx:compile translateif 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.
| Use | Classes |
|---|---|
| Text | text-base-content, muted: text-base-content/70, text-base-content/60 |
| Backgrounds | bg-base-100 (page, cards, fields), bg-base-200 (sunken panels), bg-base-300 |
| Borders | border-base-300, width border-(length:--border) |
| Brand colors | bg-primary text-primary-content, secondary, accent, neutral, each with its -content partner |
| States | info, success, warning, error (with -content) |
| Links | text-link, hover:text-link. Keep text-primary for icons and states. |
| Corners | rounded-box (cards, panels), rounded-field (inputs), rounded-selector (checkbox, badges), rounded-button |
| Shadows | shadow-card, shadow-menu, shadow-dialog, shadow-thin (scaled by the design's Depth) |
| Widths | max-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:
| Control | Classes |
|---|---|
| Button | l-btn l-btn-primary; secondary: l-btn l-btn-outline l-btn-secondary; sizes l-btn-sm, l-btn-lg |
| Text field | l-input, with an error: l-input-error |
| Select, textarea | l-select, l-textarea |
| Checkbox, radio | l-checkbox l-checkbox-primary, l-radio l-radio-primary |
| Card, table | l-card l-card-border, l-table |
| Loading placeholder | l-skeleton |
| Inline notice | l-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 setting | CSS variable | Classes that follow it |
|---|---|---|
| Colors › Button, Button text | --color-primary, --color-primary-content | bg-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-content | bg-secondary, text-secondary-content, l-btn-secondary |
| Colors › Link | --color-link | text-link, hover:text-link |
| Colors › Text | --color-base-content | text-base-content and its opacities (/70, /60); the page's default text |
| Colors › Page background | --color-base-100 | bg-base-100; the background of l-input, l-select, l-textarea, l-card |
| Colors › Surface | --color-base-200 | bg-base-200 |
| Colors › Border | --color-base-300 | border-base-300, divide-base-300; the border of l-input, l-select, l-textarea |
| Colors › Dark surface, Dark surface text | --color-neutral, --color-neutral-content | bg-neutral, text-neutral-content |
| Colors › Info, Success, Warning, Error (and their text) | --color-info … --color-error-content | bg-success, text-error, border-warning…, l-alert, l-input-error |
| Typography › Body font scale | root font-size | every rem size: the text-* scale, spacing and widths in rem |
| Typography › Heading font scale | --heading-scale | text-lg to text-5xl |
| Layout › Site, Content, Narrow width | --width-site, --width-content, --width-narrow | max-w-site, max-w-content, max-w-narrow; the frame of Group and align wide/full |
| Layout › Header height | --spacing-header | min-h-header, h-header |
| Shape › Corners — boxes | --radius-box | rounded-box; l-card, l-alert |
| Shape › Corners — buttons | --radius-button | rounded-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 | --border | border-(length:--border); the borders of l-btn, l-input, l-select, l-textarea, l-checkbox, l-radio |
| Shape › Control height | --size-field | the height of l-btn, l-input, l-select |
| Shape › Small control size | --size-selector | the size of l-checkbox, l-radio |
| Shape › Depth | --depth | shadow-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 of | Use |
|---|---|
ml-*, mr-*, pl-*, pr-* | ms-*, me-*, ps-*, pe-* |
left-*, right-* | start-*, end-* |
text-left, text-right | text-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-fullfor 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
translateXorscrollLeftcan be fixed withdir="ltr"; buttons outside it still mirror. - In JavaScript that depends on the horizontal direction (swipe handling), read
getComputedStyle(el).direction === 'rtl'on the element, notdocument.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_htmlcache. 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.