Appearance
Themes and Overrides
Lynx themes are regular Magento themes with a Vite build inside. This page covers the two things you do with them: replacing a module's component from a theme, and creating your own child theme to hold those changes.
The Lynx themes
| Theme | Code | Parent | Role |
|---|---|---|---|
| Lynx Blank | OOCS/LynxBlank | none | Magento-level files of the Lynx storefront: root.phtml, etc/view.xml image sizes, email CSS, logo. It has no storefront build of its own, so do not assign it to a store view. |
| Lynx Vue | OOCS/LynxVue | OOCS/LynxBlank | The Vue 3 storefront build |
Any theme whose parent chain reaches a Lynx theme is a Lynx theme. Your child theme therefore gets everything the parent has: the PageBuilder editor and preview, designs, Lynx page handling, SSR.
A Lynx storefront theme has this layout:
<theme>/
├── registration.php, theme.xml, composer.json
├── Magento_Theme/layout/lynx_default.xml ← stylesheet and font preloads
├── OOCS_LynxInertia/templates/inertia.phtml ← script tags of the bundle
├── OOCS_LynxCatalog/lynx/vue/src/… ← component overrides, per module (optional)
├── lynx/ ← the Vite build, its own npm project
│ ├── package.json, vite.config.js, tsconfig.json
│ └── src/
│ ├── main.ts ← browser entry: styles.css, then renderStorefront()
│ ├── ssr.ts, ssr-worker.ts ← SSR entries (Node, Cloudflare Worker)
│ └── styles.css ← Tailwind entry, fonts, theme tokens
└── web/ ← build output only: js/lynx/, css/lynx/Source files never go in web/: Magento's static deploy publishes everything there.
Overriding components
A theme replaces a file of any module by placing a file with the same path under a folder named after the module. The module is not touched, and the change applies only to store views that use the theme.
Module file: <module>/view/frontend/lynx/vue/src/<path>
Theme override: <theme>/<Vendor_Module>/lynx/vue/src/<path>| Module file | Override in a theme |
|---|---|
module-lynx-catalog/view/frontend/lynx/vue/src/Components/Product/Simple.vue | <theme>/OOCS_LynxCatalog/lynx/vue/src/Components/Product/Simple.vue |
module-lynx-theme-component/view/frontend/lynx/vue/src/Components/Header.vue | <theme>/OOCS_LynxThemeComponent/lynx/vue/src/Components/Header.vue |
module-lynx-theme/view/frontend/lynx/vue/src/stores/global/messagesStore.ts | <theme>/OOCS_LynxTheme/lynx/vue/src/stores/global/messagesStore.ts |
You can override any number of files; everything you do not override keeps coming from the module.
How the build picks a file
Each file is known by a key: the module name plus its path in src/ (OOCS_LynxCatalog/Components/Product/Simple.vue). When several folders have a file with the same key, the first one in this order wins:
- The theme's module overrides,
<theme>/<Vendor_Module>/lynx/vue/src/ - The module overrides of the parent theme, then of its parent, and so on up the chain
- The module,
<module>/view/frontend/lynx/vue/src/
The theme's own lynx/src/ holds the entry files. Components placed there have keys without a module name (Components/Promo.vue), so they never replace a module file.
So an override in a parent theme applies to its child themes too, and a child theme can override it again.
The losing file is not bundled at all. Every import of the key gets the winner, wherever it comes from: the component registry, import … from 'OOCS_LynxCatalog/…' in another module, and relative imports inside the module itself. That is why overriding a store (.ts) changes it for every component that uses it.
Overrides of a module that is disabled, or not in the build, are skipped. After each build, the log lists the chunks built from your theme's overrides; seeing your file there confirms the override was used (-v lists every chunk):
[lynx] 94 chunks · JS 1250.9 kB (407.3 kB gzip) · CSS 118.7 kB (19.4 kB gzip) · 2.1s
[lynx] built from theme overrides:
OOCS_LynxCatalog/Components/Product/Simple.min.js ← app/design/frontend/Vendor/storefront/OOCS_LynxCatalog/lynx/vue/src/Components/Product/Simple.vueWhen the component you override has containers or events, keep its Container tags and dispatch() calls in your copy, or the build warns and other modules' additions stop running there.
Type checking follows the same order: the generated tsconfig.modules.json points each OOCS_Module/* path to the theme's override folder, then the parent themes' override folders, then the module's.
Example: change the product page
The Product Detail component for simple and virtual products is OOCS_LynxCatalog::Components/Product/Simple. To change its markup in your theme (here app/design/frontend/Vendor/storefront, see Creating a child theme):
Copy the module file into the override folder, keeping its path:
bashmkdir -p app/design/frontend/Vendor/storefront/OOCS_LynxCatalog/lynx/vue/src/Components/Product cp vendor/oocscommerce/module-lynx-catalog/view/frontend/lynx/vue/src/Components/Product/Simple.vue \ app/design/frontend/Vendor/storefront/OOCS_LynxCatalog/lynx/vue/src/Components/Product/Edit the copy. Keep the props it declares and the slots it renders (
price_after,product.info.additional,info_after): the provider still sends the same props, and merchants may have placed components in those slots. Treaturls.add_to_wishlistandurls.add_to_compare(and their labels) as optional: the provider sends them only while the component's Show Wishlist or Show Compare setting is on and the store allows the feature, and the buttons show only when they are there. The images are not in this component: to change the gallery, overrideComponents/Product/Media(Product Media), and for the breadcrumbs or the columns around the images and the details,Components/Product/Page(Product).Build the theme and clean the cache:
bashbin/magento lynx:static:deploy Vendor_storefront bin/magento setup:static-content:deploy # production mode bin/magento cache:cleanThe override is picked up at build time. A new override file needs no
lynx:theme:setup; a new theme does (see below). During development,npm run devin the theme'slynx/folder picks up added and removed files without a restart (Setup).Check the build log for the override path, then open a simple product page.
To show data the provider does not send yet, add it with an after plugin on the provider's resolve(), then read the new prop in your override. Lynx calls resolve() through the object manager, so plugins on providers run:
xml
<!-- Vendor_Module etc/di.xml -->
<type name="OOCS\LynxCatalog\Provider\Product\Detail\Simple">
<plugin name="vendor_module_delivery_note" type="Vendor\Module\Plugin\AddDeliveryNote"/>
</type>php
<?php
declare(strict_types=1);
namespace Vendor\Module\Plugin;
use OOCS\LynxCatalog\Provider\Product\Detail\Simple;
class AddDeliveryNote
{
public function afterResolve(Simple $subject, array $result, array $props): array
{
$result['delivery_note'] = (string) __('Ships within 24 hours');
return $result;
}
}Things to know
- An override replaces the whole file. There is no way to import the original and extend it: a relative import of the module's file from your override resolves to your override. Copy the file and change the copy.
- Keep the contract. Props, slot names and, for stores and utilities, every export that other files import must stay. Type checking catches a missing export or a changed type; it does not catch a changed behavior.
- Recheck overrides when you update Lynx. A new version may change the module file you copied. Compare your copy with the new original after
composer update. - Prefer an extension point when there is one. Many changes need no override: place components in the slots of the product, category, cart and checkout components, add data with a plugin on a provider, or register a new component (Creating Components). Override when you need to change markup or behavior the component does not expose.
- Keep SSR working. An override renders on the server too when SSR is on: no
windowordocumentduring setup or render. - CSS files are not overridden file by file. Change styles from the theme's
styles.cssinstead.
Creating a child theme
Put your overrides, fonts and tokens in a child theme of Lynx Vue, never in the Lynx packages, so updates do not overwrite them. This walkthrough creates Vendor/storefront, a child of Lynx Vue.
1. Register the theme
app/design/frontend/Vendor/storefront/
├── registration.php
├── theme.xml
└── composer.jsonregistration.php:
php
<?php
declare(strict_types=1);
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::THEME, 'frontend/Vendor/storefront', __DIR__);theme.xml:
xml
<?xml version="1.0"?>
<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
<title>Vendor Storefront</title>
<parent>OOCS/LynxVue</parent>
</theme>composer.json (needed when you install the theme as a package; the require keeps the parent installed):
json
{
"name": "vendor/theme-frontend-storefront",
"type": "magento2-theme",
"require": {
"oocscommerce/theme-frontend-lynx-vue": "^1.0"
},
"autoload": {
"files": ["registration.php"]
}
}2. Add the Vite build
The child theme builds its own bundle, so it needs its own lynx/ folder. Copy the parent's build files:
bash
T=app/design/frontend/Vendor/storefront
P=vendor/oocscommerce/theme-frontend-lynx-vue
mkdir -p $T/lynx/src $T/web
cp $P/lynx/package.json $P/lynx/vite.config.js $P/lynx/tsconfig.json $P/lynx/.gitignore $T/lynx/
cp $P/lynx/src/main.ts $P/lynx/src/ssr.ts $P/lynx/src/ssr-worker.ts $P/lynx/src/styles.css $T/lynx/src/
cp $P/web/.gitignore $T/web/
# Only for SSR on a Cloudflare Worker: also copy wrangler.tomlCopy eslint.config.mjs too if you lint. The three entry files only call the storefront code of OOCS_LynxTheme (renderStorefront(), startSsrServer(), the Worker handler), so they follow Lynx updates without changes; styles.css is the one you edit.
Then edit lynx/package.json: give it your own name, for example "name": "vendor-storefront". Keep the scripts and "workspaces": [".lynx"] as they are; setup needs workspaces. The build plugins come from the Lynx module, which setup installs into the theme.
The copied vite.config.js works unchanged. It reads the enabled modules and the parent chain from what setup writes, so your theme gets the parent's overrides and every module's components.
The parent's inertia.phtml, lynx_default.xml, root.phtml and etc/view.xml are inherited through Magento's theme fallback; you do not copy them. inertia.phtml loads js/lynx/main.min.js through the theme, so it picks up your theme's build.
3. Set up the theme
Register the theme with Magento, then set it up:
bash
bin/magento setup:upgrade
bin/magento lynx:theme:setup Vendor_storefrontThe theme name to type is its code with _ in place of /. bin/magento lynx:theme:setup --list shows the Lynx themes the installation has. The command writes the theme's packages (the Lynx build kit and the enabled modules) and its parents into lynx/.lynx/package.json, and runs npm install in the theme's lynx/ folder, which gets its own node_modules. Run it again after enabling or disabling a module; without names it sets up every theme set up before.
4. Customize
Override components as described in Overriding components, under app/design/frontend/Vendor/storefront/<Vendor_Module>/lynx/vue/src/.
Styles and tokens. lynx/src/styles.css is the theme's Tailwind entry. It imports the shared Lynx styles and lists every token with its default, commented out; uncomment a line to change it:
css
@theme {
/* The theme font, self-hosted below */
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
/* Corner of buttons when a page has no design */
--radius-button: 9999px;
}Design settings that the merchant sets in the editor (colors, corners, borders, widths, heading scale) are written on every page and win over the values in styles.css. Those values are the fallback for a page without a design. Put the brand palette in the store's design, and use styles.css for what designs do not set: the font, the type scale, extra tokens and global CSS.
Fonts. The parent self-hosts Mulish. To use another font, add it as a dependency of your theme (for example npm install @fontsource/inter in its lynx/ folder), replace the @font-face rules and --font-sans in styles.css, and replace the parent's font preloads in Magento_Theme/layout/lynx_default.xml. The build copies font files into web/js/lynx/ under their own names.
xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<remove src="js/lynx/mulish-latin-400-normal.woff2"/>
<remove src="js/lynx/mulish-latin-600-normal.woff2"/>
<link src="js/lynx/inter-latin-400-normal.woff2" rel="preload" as="font" crossorigin="anonymous"/>
</head>
</page>Image sizes. Add etc/view.xml only for the image sizes you change; the rest comes from Lynx Blank. Keep the image ids the Lynx components use (category_page_grid, product_page_image_large…).
Magento templates. Templates like Magento_Theme/templates/root.phtml follow normal Magento theme fallback: copy the file from Lynx Blank into the same path of your theme and edit it.
5. Build and deploy
bash
bin/magento lynx:static:deploy Vendor_storefront
bin/magento setup:static-content:deploy # production mode, after the Lynx build
bin/magento cache:cleanlynx:static:deploy runs npm run build (type check, then Vite) in your theme's lynx/ folder and writes the bundle to the theme's web/js/lynx/ and web/css/lynx/. Run setup:static-content:deploy after it, never before, or Magento publishes the old bundle. Without a name, lynx:static:deploy builds every theme set up; add --ssr to build the SSR bundle too (Server-side Rendering).
6. Assign the theme
- Go to Content › Design › Configuration and edit the store view.
- Set Applied Theme to Vendor Storefront and save.
- Clean the cache.
Designs are not tied to a theme. The store view keeps its Active Design (Stores › Configuration › Lynx › Theme Settings), the editor opens and previews it as before, and switching between Lynx Vue and your theme changes only the code that draws the pages, not their layouts or settings.