Skip to content

Third-party extensions under Nebula

Nebula replaces the admin's rendering layer, not Magento. An extension's models, controllers, routes, ACL and configuration keep working. Its menu entries keep working too, unless they hang from one of the stock menu containers Nebula removes; see Menu parents Nebula removes. What is gone is the stock admin's LESS pipeline, RequireJS, Knockout and the Prototype.js globals. This page says what that means for an extension you already run, how to check one, and what to do when it does not fit.

What keeps working

Part of an extension Under Nebula
Controllers, routes, ACL resources Unchanged.
Menu entries Rendered by Nebula's menu with a default icon. An icon can be registered in DI; see below. An entry parented under one of the 24 stock containers Nebula removes disappears instead; see Menu parents Nebula removes.
Configuration sections from system.xml Rendered by Nebula's configuration form: groups, fields, comments, depends, array fields. A custom frontend model that renders HTML on the server renders, but in stock markup rather than a Nebula row; see Configuration fields and custom frontend models. JavaScript it expected RequireJS to run does not.
Server-rendered templates Render inside Nebula's page frame.
The "Test Connection" button on search engine settings Works. Nebula reads the button's data-mage-init payload natively.
Inline onclick handlers calling setLocation(), $(), deleteConfirm(), confirmSetLocation(), disableElements(), enableElements() Work. Nebula ships small native replacements for these legacy globals.
Database, cron, API, storefront Untouched. Nebula is adminhtml only.

What does not

Extension ships What happens Signal
Admin LESS at view/adminhtml/web/css/source/_module.less Never compiled. Nebula has no parent theme and builds its stylesheet with Tailwind. The markup arrives with no rules. Nothing in any log. The page looks unstyled.
A UI Component grid or form that Nebula does not replace The layout reads it and the container renders, but the Knockout application that fills it never starts. An empty box where the grid should be.
A ui_component/<name>.xml that adds a column or field to a core component Nebula does replace, such as a column on customer_listing, sales_order_grid or cms_page_listing Nebula skips the whole component by name, and the extension's merged XML goes with it. Nebula's own grid renders in its place, correct but without the extension's column. Nothing in any log, no console line, nothing from nebula:doctor. The column is simply absent.
A button or field declared on a core UI component Nebula replaces, such as customer_form, product_form or the listing components The declaration is read into a component that is never scheduled and never rendered, so the control simply is not there. Nothing anywhere: no console line, no log entry, no empty box.
Scripts that call require([...]), define(), or use x-magento-init The call is swallowed. Browser console: [Nebula] [dead-require] naming the modules asked for.
Inline handlers calling toggleValueElements, Fieldset, fireEvent Accepted and ignored. Browser console: [Nebula] [inert-shim] naming the global.
Code that expects jQuery, Prototype.js or Knockout globals Absent. A ReferenceError in the console.

data-mage-init is handled only for the test-connection button. Every other use is inert.

The two rows about a dropped column and a dropped control are the only failures with no console signal and nothing visibly wrong. Unstyled markup announces itself; a missing column or a missing button does not. The only way to find them is to compare the screen against the same screen under the stock admin theme.

Nebula's own menu removes 24 stock containers. A third-party entry parented under one of them disappears silently. Nothing is logged, because a parent that exists but is marked removed makes Magento's menu builder drop the child rather than raise.

Removed by Ids
Nebula_Menu Magento_Catalog::inventory, Magento_Backend::stores_attributes, Magento_Sales::sales_operation, Magento_CatalogRule::promo, Magento_Backend::marketing_communications, Magento_Backend::marketing_seo, Magento_Backend::marketing_user_content, Magento_Backend::content_elements, Magento_Backend::system_design, Magento_MediaGalleryUi::media, Magento_Backend::stores_settings, Magento_Tax::sales_tax, Magento_Backend::other_settings, Magento_Analytics::advanced_reporting, Magento_Analytics::business_intelligence, Magento_Marketplace::partners
Nebula_Reports Magento_Reports::report_salesroot, Magento_Reports::report_products, Magento_Reports::report_customers, Magento_Reports::report_marketing, Magento_Reports::report_statistics, Magento_Review::report_review
Nebula_System Magento_Variable::system_variable, Magento_AdminNotification::system_adminnotification

If your menu.xml parents under one of these, re-parent it onto a container Nebula keeps, or onto a top-level id.

Removed parent Re-parent onto
Magento_Backend::content_elements Magento_Backend::content
Magento_Sales::sales_operation Magento_Sales::sales
Magento_Tax::sales_tax Nebula_Menu::stores_taxes
Magento_Backend::stores_settings, Magento_Backend::other_settings Magento_Backend::stores

Configuration fields and custom frontend models

Nebula renders the configuration form by replacing core blocks through DI preferences in code/Nebula/Core/etc/adminhtml/di.xml: Magento\Config\Block\System\Config\Form, Magento\Config\Block\System\Config\Form\Field, Magento\Config\Block\System\Config\Form\Fieldset and Magento\Backend\Block\Widget\Form\Element\Dependence.

A preference binds one class, not its subclasses. A frontend_model in your system.xml that extends Magento\Config\Block\System\Config\Form\Field therefore runs core's render(). The field's own HTML still appears, but wrapped in core's <table>, <tr> and <td> markup rather than Nebula's nebula-config-field label and value grid, and without the Alpine "Use system value" inherit toggle. It renders; it does not match the rows around it.

Keep your logic in _getElementHtml() and let Nebula's row draw the chrome. Extend Nebula\Core\Block\Config\Form\Field when the module requires Nebula, or keep extending the core class and accept the stock row when it must run on both admins. Nebula itself ships per-class preferences for specific vendor renderers, such as the media storage synchronize field and the Adobe Payment Services fieldsets, and a third party can do the same for its own class.

For the control inside the field, the nebula-btn classes, the alpine:init registration and window.Nebula.postAction(url) are documented on CSS and JavaScript in your module. A button that relies on data-mage-init does nothing.

How to check an extension

  1. Install it on a staging store that runs Nebula.
  2. Open every admin page the extension adds or changes: its menu entries, its configuration section, every grid and form, and any button it adds to a core screen.
  3. Open the browser console and filter for [Nebula]. Each [dead-require] line names a RequireJS module a page asked for; each [inert-shim] line names a legacy global it called; each [fail-open] line, in developer mode only, names a form that submitted without client-side validation. A clean console does not clear a screen the extension only adds controls to.
  4. Apply the four-point check to each screen: it renders, its primary action completes, invalid input is rejected with a message, and the console has no errors from the extension's own code.
  5. For every core grid or form the extension extends, open the same screen under the stock admin theme with the theme switcher and compare the columns, fields and buttons. Anything present there and missing under Nebula was dropped with the component.
  6. Run bin/magento nebula:doctor. It reports on the installed Nebula_Core* modules only, so it says nothing about definitions an extension ships.
  7. Check the extension's own definitions with bin/magento nebula:lint --path=app/code/Vendor/Module. Plain bin/magento nebula:lint validates every enabled module. The grid schema requires dataSource.provider to be a lowercase dotted alias, so a provider written as a class name fails there. A dataSource.config.collection written as a class name is caught by neither command on a third-party module, because the schema accepts any string for it and doctor's collection metric scans Nebula's own modules. That one shows up only when the page is opened, and quietly. The provider catches the UnknownAliasException, logs NebulaGrid: unknown collection alias and returns no rows, so the grid renders its chrome and columns with nothing in them. See Extension points. Opening every screen stays the real test for it.

What to do

Symptom Fix
Unstyled markup A plain CSS file over Nebula's tokens, loaded from layout XML. Ask the vendor, or ship it in a small module of your own. See CSS and JavaScript in your module.
Empty UI Component grid or form Enable the UI Bridge, included in nebula/admin, and test the converted screen. For an unsupported component or a definition you want to maintain yourself, use bin/magento nebula:ui:transform and finish the generated definition and layout.
A vendor column or field is missing from a core grid or form Re-add it as an overlay: a view/adminhtml/grid/<same id>.json or view/adminhtml/form/<same id>.json in your own module carrying just that column or field, sequenced after the module that ships the base. See Extending a shipped grid and Extending a shipped form. Add the data behind it as well, because the overlay declares the column but does not fill it. See Data providers.
A vendor button or field is missing from a core screen Rebuild it as a Nebula snippet, or as a form overlay against the Nebula definition for that screen. See Snippets and Extending a shipped form. The component the vendor declared it on does not exist under Nebula.
A server-rendered core form posts without client-side validation Mark it with data-nebula-legacy-validate. See Validation on a server-rendered core form.
Dead require() calls Rewrite the behaviour as a linked, deferred script that registers on alpine:init, or as native HTML. The theme ships no module loader.
Needs jQuery or Prototype.js Bundle what the script needs inside your own script. Nothing on the page provides them.
A screen you need today and cannot fix today Switch the admin theme back with the theme switcher while you use it, then switch back. The switch is store-wide, not per page. See Upgrade, opt out, remove.

On EAV screens the loss is narrower than it looks. An extension that adds a product or customer attribute still renders, because groups no node claimed fall through to the remaining-groups strip. See EAV forms. What is lost is a non-attribute field declared in ui_component/product_form.xml or customer_form.xml. There the overlay target is Nebula's own id, form/catalog_product_edit.json or form/customer_edit.json, not the stock component name.

Validation on a server-rendered core form

The symptom is a server-rendered admin form that posts without client-side validation. The rules are on the fields, but an empty submit round-trips and comes back as a server error naming no field. Its validation used to come from an inline require(['jquery', 'mage/mage'], ...) call binding mage('validation'), which Nebula swallows.

The fix is one attribute, not a script. Put data-nebula-legacy-validate on the form, or on any ancestor of it. Nebula installs one delegated submit listener that, for any form inside a marked element, runs window.Nebula.validateForm(), blocks the submit when it fails, opens every <details> holding a failed field, then scrolls to and focuses the first. Fields that already carry Magento's validation classes, such as required-entry or validate-email, need nothing further. Nebula translates that class vocabulary into its own data-validate on every admin page. Core's brace-syntax data-validate is not read.

Check first whether you need it

Nebula overrides Magento_Backend::widget/form/container.phtml, the default template for every Widget\Form\Container, and that override already wraps the form in the attribute. A container screen using the stock template is covered. You need the attribute only when your module overrides that template, or renders its own <form> in a plain template.

The attribute usually has to sit on an ancestor rather than on the form. A form emitted through Widget\Form\Container::getFormHtml() is rendered by Framework\Data\Form, which writes a fixed attribute list of id, action, method, enctype, class, onsubmit and target, and has no supported way to take a data attribute from a template. Wrap the form in a marked <div> instead.

It fails open on purpose. If the bundle did not load, window.Nebula.validateForm is missing and the submit is allowed through, and the server validates regardless. The reason is logged once per form as [Nebula] [fail-open], in developer mode only.

For extension authors: shipping Nebula support

An extension can support Nebula and the stock admin from one package. Nothing below breaks a store that does not have Nebula installed.

Ship a form definition next to the UI Component. Put view/adminhtml/form/<component name>.json in the module, named exactly after the <uiComponent name="...">. A form definition that carries route mounts itself: Nebula matches the request's full action name against every module's form definitions and injects its own form block, plus the removals named in replaces, at runtime. No layout file is needed. Without Nebula nobody reads the file.

A grid definition does not mount itself. Suppression only unschedules the stock <uiComponent>, so view/adminhtml/grid/<component name>.json on its own leaves an empty content area. The module must also ship layout XML, on the handle that declared the component, mounting the Nebula grid block:

view/adminhtml/layout/acme_thing_index.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">
    <body>
        <referenceContainer name="content">
            <block class="Nebula\Grid\Block\Grid"
                   name="nebula.acme_thing_listing"
                   after="-">
                <arguments>
                    <argument name="grid_id" xsi:type="string">acme_thing_listing</argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

bin/magento nebula:ui:transform emits this file as a layout draft, with a generated comment line above the container. That block class only exists when Nebula is installed, so the layout file cannot ship in the base module. Put it, with the collection and provider DI bindings, in a Nebula-only sub-module that requires the Nebula packages and sequences after the base module. The UI Bridge is the one exception, because it swaps the scheduled element for a Nebula block itself, and its nebula/module-ui-bridge package is included in the metapackage. Runtime conversion is disabled by default and must be enabled in configuration. An extension that wants to work on a plain Nebula install still ships the layout XML.

Bind the collection and provider aliases in the sub-module's etc/di.xml — the global file, never etc/adminhtml/di.xml. The registries only exist when Nebula is installed, which is why the bindings live in a Nebula-specific sub-module that requires the Nebula packages (or the extension simply requires Nebula). An area file is not a way to guard them: Magento merges one area's di.xml files deeply, but an argument declared in an area file replaces the merged global declaration for that area, so an etc/adminhtml/di.xml naming DataProviderRegistry or CollectionRegistry would drop every Nebula grid's own bindings for adminhtml requests. bin/magento nebula:doctor reports any module that does it. See Extension points.

Sequence after Nebula when present. <sequence> entries in module.xml that name a module which is not installed are ignored, so listing Nebula_Component, Nebula_Grid and Nebula_Form is safe either way.

Style with plain CSS on tokens. A stylesheet loaded from layout XML works on both admins. Use var(--nebula-*, <fallback>) with a fallback value so the stock admin gets a colour too, and do not reach for Tailwind utilities in your markup: the theme does not scan your templates, so they are emitted nowhere. See CSS and JavaScript in your module.

Script without a loader. A linked script that registers its Alpine component on alpine:init and does nothing when Alpine is absent runs on both admins; place it after nebula.component.scripts in before.body.end, or deferred in <head>. Keep RequireJS-dependent code in a separate file that only the stock admin's layout loads. window.Nebula.t() translates through the same dictionary Nebula's bundles use, fed by JsPhrasesProviderInterface, and web/types/nebula.d.ts types the surface. See CSS and JavaScript in your module.

Register a menu icon.

etc/di.xml
<type name="Nebula\Core\Model\Menu\MenuIconRegistry">
    <arguments>
        <argument name="icons" xsi:type="array">
            <item name="Acme_Catalog::dashboard" xsi:type="array">
                <item name="class" xsi:type="string">fa-solid fa-rocket</item>
            </item>
            <item name="Acme_Catalog::brand" xsi:type="array">
                <item name="type" xsi:type="string">image</item>
                <item name="src" xsi:type="string">Acme_Catalog::images/logo.svg</item>
            </item>
        </argument>
    </arguments>
</type>

The class form takes a Font Awesome class; the image form takes a static asset path. In etc/di.xml, for the same reason as every registry: an area file's icons argument would replace the global one and drop every other module's icons.

Ask whether Nebula is rendering. Nebula\Core\Api\V1\AdminThemeStateInterface::isNebulaRendering() answers false when the merchant has switched to another admin theme. A block that renders differently under Nebula should ask it rather than check the theme path.

Declare an acl on every definition and keep the field whitelist in your save controller. The engine refuses a definition without acl for everyone.

Optional core modules

Some report and menu screens Nebula ships reference optional core modules such as PayPal, Braintree and Reviews. On an install with those modules disabled the screens fall back rather than fail: the menu entries that borrow them declare dependsOnModule, and the two pending-reviews providers are guarded and proxied. Removing the packages is different. Nebula's code imports classes from Paypal, Review, CheckoutAgreements, AsynchronousOperations, Newsletter and the Inventory (MSI) API modules — InventoryApi, InventoryCatalogApi, InventorySalesApi, InventoryAdminUi — so the manifests require them and Composer refuses composer remove; without them setup:di:compile would fail. Third-party modules that extend those screens are affected the same way the screens are.