BA_Svelte is the shared Magento 2 Svelte platform for BA storefront modules.
In practice a BA storefront module built on BA_Svelte usually looks like this:
- Layout XML declares a root
SvelteBlock - That root points at a
.sveltecomponent in your module - XML arguments, a
view_model, andcomputed_propsbecome component props - Optional view xml child blocks/containers become named Svelte slots
- The shared runtime mounts the root component on the page
- Your Svelte code imports shared BA platform helpers through
@modules
That keeps the authoring model simple:
- normal Magento layout and blocks
- normal Svelte components
- no extra registry file
- no second naming system
- Magento-native
before,after,move, andremovestill work
Simply, can be condensed to:
BA\Svelte\Block\SvelteBlockAny svelte block on frontend. Can be root or child components.view/frontend/templates/root.phtmlEmits the.svelte-rootwrapper, serialized config, and optional server fallback markup.view/frontend/web/svelte-srcShared Vite source used to build the runtime bundle against the deployed static-content tree.
To build your svelte interface, and run bin/magento setup:static-content:deploy -f --jobs=20 --area=frontend to show on frontend. No need to rm -rf generated or preprocessed folders.
All pieces:
BA\Svelte\Block\SvelteBlockAny svelte block on frontend. Can be root or child components.BA\Svelte\Block\SvelteLinkRoot-style Svelte block for sorted Magento link collections such astop.links.view/frontend/templates/root.phtmlEmits the.svelte-rootwrapper, serialized config, and optional server fallback markup.view/frontend/web/svelte-srcShared Vite source used to build the runtime bundle against the deployed static-content tree.view/frontend/web/js/lib/i18n.jsPublic translation helper.view/frontend/web/js/lib/messages.jsPublic message and Magento fragment update facade.view/frontend/web/js/lib/magento.jsBETA Public Magento URL and JSON request helper facade.view/frontend/web/js/lib/state.jsBETA Public customer-section and store hydration facade.view/frontend/web/js/lib/forms.jsALPHA Public validation and AJAX form facade.view/frontend/web/js/lib/commerce.jsTODO Public add-to-cart facade.BuildSveltePluginHooks intosetup:static-content:deployand builds the merged storefront Svelte bundle in each deployed static-content root that containsBA_Svelte.
The normal starting point is one root block in layout XML.
<block class="BA\Svelte\Block\SvelteBlock"
name="example.root"
<!-- Root must use a phtml template. Only child blocks can use svelte templates here, and leave the `svelte_component` argument -->
template="BA_Svelte::root.phtml">
<arguments>
<argument name="svelte_component" xsi:type="string">Vendor_Module::example-root.svelte</argument>
<argument name="heading" xsi:type="string">Example</argument>
<argument name="computed_props" xsi:type="array">
<item name="endpointUrl" xsi:type="array">
<item name="resolver" xsi:type="string">url</item>
<item name="path" xsi:type="string">rest/V1/example</item>
</item>
</argument>
</arguments>
<container name="example.root.container.content" as="content"/>
<container name="example.root.container.actions" as="actions"/>
</block>svelte_component uses Magento template notation:
Vendor_Module::example-root.svelte
That resolves to:
view/frontend/web/svelte/example-root.svelte
The matching root component can then read normal props plus the container helpers:
<script>
import ContainerRenderer from '@modules/BA_Svelte/svelte/container-renderer.svelte';
let {
heading = 'Example',
get_container: getContainer,
} = $props();
</script>
<section>
<h2>{heading}</h2>
<ContainerRenderer items={getContainer?.('content') ?? []} />
</section>Obviously, as its just view xml, a second module can extend the xml we added above:
<referenceContainer name="example.root.container.actions">
<block class="BA\Svelte\Block\SvelteBlock"
name="example.root.primary_action">
<arguments>
<argument name="svelte_component" xsi:type="string">Vendor_Module::actions/primary-button.svelte</argument>
<argument name="label" xsi:type="string">Continue</argument>
</arguments>
</block>
</referenceContainer>Given the parent module uses $state in a seperate file imported using @modules, you can bring that in, and change the state from your added component.
Every mounted component receives:
- normal props from XML arguments and
computed_props containersget_container(name)has_container(name)default_container
Render nested configs through:
@modules/BA_Svelte/svelte/container-renderer.svelte@modules/BA_Svelte/svelte/block-renderer.svelte@modules/BA_Svelte/svelte/renderer.svelte
If you need to render one child block directly and optionally override props inline:
<script>
import BlockRenderer from '@modules/BA_Svelte/svelte/block-renderer.svelte';
let {
default_container: defaultContainer,
} = $props();
</script>
<BlockRenderer block={defaultContainer} value={row.value} />You can also target a child by layout block name:
<BlockRenderer block="price_renderer" blocks={defaultContainer} value={row.value} />Root mounts can optionally render server HTML first and then let the runtime progressively enhance it.
To do that, add a normal Magento child block with alias fallback under the root SvelteBlock.
Example:
<block class="BA\Svelte\Block\SvelteBlock"
name="example.root"
template="BA_Svelte::root.phtml">
<arguments>
<argument name="svelte_component" xsi:type="string">Vendor_Module::example-root.svelte</argument>
</arguments>
<block class="Magento\Framework\View\Element\Template"
name="example.root.fallback"
as="fallback"
template="Vendor_Module::example/fallback.phtml"/>
</block>Behavior:
- the fallback markup stays visible initially
- BA_Svelte mounts the Svelte app into a hidden host inside the same root wrapper
- after the first successful client render, the runtime removes the fallback and reveals the mounted app
- if config parsing or mount fails, the fallback stays in place
This is root-mount behavior only. It is not a Svelte hydration contract and it does not preserve DOM identity between server HTML and the client component.
Sometimes you may need to render a svelte block without using the svelte block class. If you bring in BA\Svelte\ViewModel\SvelteBlockRenderer as a viewmodel, you can call render(). This wont include child components, but will in future.
If the component needs to participate in link collections that expect SortLinkInterface, use BA\Svelte\Block\SvelteLink.
<referenceBlock name="top.links">
<block class="BA\Svelte\Block\SvelteLink"
name="example.account.link"
after="my-account-link">
<arguments>
<argument name="sortOrder" xsi:type="number">60</argument>
<argument name="svelte_component" xsi:type="string">Vendor_Module::account-link.svelte</argument>
<argument name="view_model" xsi:type="object">Vendor\Module\ViewModel\AccountLink</argument>
</arguments>
<action method="setTemplate">
<argument name="template" xsi:type="string">Vendor_Module::link.phtml</argument>
</action>
</block>
</referenceBlock>Use this for:
top.links- customer account link groups
- theme-specific link builders that sort children through Magento link APIs
There are three normal ways to get data into a component:
Any non-reserved XML argument on a SvelteBlock is normalized into component props.
<argument name="heading" xsi:type="string">Example</argument>
<argument name="show_summary" xsi:type="boolean">true</argument>You can attach a Magento view_model and let BA_Svelte reflect its getter methods into props.
<argument name="view_model" xsi:type="object">Vendor\Module\ViewModel\Example</argument>Example view model:
<?php
declare(strict_types=1);
namespace Vendor\Module\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
class Example implements ArgumentInterface
{
/**
* @return string
**/
public function getMessage(): string
{
return 'Hello from Magento';
}
}That becomes:
message
Reflection rules:
getCountryOptions()becomescountry_optionsisGuestCheckoutAllowed()becomesguest_checkout_allowedhasShippingPolicy()becomesshipping_policy
Only public zero-argument getter methods are included. Reflected view_model props are merged before computed_props, and explicit XML props still win if keys collide.
Use resolver-backed props for values that should be calculated at render time.
Built-in resolvers:
urlassettranslate
You can also list all registered resolvers using the ba:svelte:resolver:list command.
Example:
<argument name="computed_props" xsi:type="array">
<item name="endpointUrl" xsi:type="array">
<item name="resolver" xsi:type="string">url</item>
<item name="path" xsi:type="string">rest/V1/example</item>
</item>
</argument>If the built-in resolvers are not enough, add your own resolver class in your module and register it into BA\Svelte\Model\PropResolverPool through DI.
Example resolver:
<?php
declare(strict_types=1);
namespace Vendor\Module\Model\PropResolver;
use BA\Svelte\Api\PropResolverInterface;
use Magento\Framework\View\Element\Template;
class ExamplePropResolver implements PropResolverInterface
{
public function resolve(string $propName, array $definition, Template $block): mixed
{
$value = $definition['value'] ?? null;
return is_string($value) ? strtoupper($value) : null;
}
}Register it in your module etc/di.xml:
<type name="BA\Svelte\Model\PropResolverPool">
<arguments>
<argument name="resolvers" xsi:type="array">
<item name="example" xsi:type="object">Vendor\Module\Model\PropResolver\ExamplePropResolver</item>
</argument>
</arguments>
</type>Then use it in layout XML:
<argument name="computed_props" xsi:type="array">
<item name="exampleValue" xsi:type="array">
<item name="resolver" xsi:type="string">example</item>
<item name="value" xsi:type="string">hello world</item>
</item>
</argument>Rules for custom resolvers:
- implement
BA\Svelte\Api\PropResolverInterface - return data that can be safely serialized into component props
- read your resolver-specific config from the
$definitionarray - use the provided Magento
$blockwhen you need store-aware URLs, assets, or request/render context
If you want to see what a mounted block will actually receive, use the CLI helper:
bin/magento ba:svelte:props checkout_index_index checkout.rootThe command loads default plus the handle you pass, resolves the named BA_Svelte block, and prints a typed Svelte 5 $props() snippet.
Use --store=<code> when the layout or computed props depend on store scope.
Svelte modules in this setup import shared code through the @modules Vite alias.
Example:
import ContainerRenderer from '@modules/BA_Svelte/svelte/container-renderer.svelte';
import { _ } from '@modules/BA_Svelte/js/lib/i18n.js';
import { buildRestUrl } from '@modules/BA_Svelte/js/lib/magento.js';@modules points at the deployed static-content root for the current storefront, not at app/code directly.
So:
import ContainerRenderer from '@modules/BA_Svelte/svelte/container-renderer.svelte';resolves against:
pub/static/<area>/<Vendor>/<Theme>/<Locale>/BA_Svelte/svelte/container-renderer.svelte
Why this matters:
- imports stay short
- one BA module can reuse public code from another BA module
- the build runs against the same merged static asset tree Magento serves
Rules:
- files under
svelte/and documentedjs/entrypoints are fair to reuse - files under
svelte-src/are build/runtime internals unless documented otherwise
Most feature modules should build on the public facades below and avoid js/lib/runtime/*, http.js, and url.js directly.
import { _ } from '@modules/BA_Svelte/js/lib/i18n.js';This uses window.baTranslate() and falls back to the original string when no Svelte translation entry exists.
BA_Svelte scans Svelte-backed .svelte, .ts, and .js source files on the Magento side and exposes a translated hashmap to the storefront runtime.
Use _('Literal phrase') for any Svelte-authored text that needs Magento translation. Dynamic expressions, concatenated strings, and template literals with expressions are not extracted into the BA_Svelte translation map, so they fall back to the original source text.
Example:
<script>
import { _ } from '@modules/BA_Svelte/js/lib/i18n.js';
let { buttonLabel = 'Open size guide' } = $props();
</script>
<button type="button">
{_(buttonLabel)}
</button>If a module needs standard price output, use the shared price component:
import Price from '@modules/BA_Svelte/svelte/catalog/price.svelte';This component handles:
- normal final price rendering
- special price plus old price output
- locale-aware currency formatting
- optional minimal price /
As low asoutput - optional minimal-price links back to the product URL
Props:
final_price, REQUIRED
regular_price, default = 0,
has_special_price, default = false,
currency_code, default = window.__baCurrentCurrency,
currency_symbol, default = '',
locale, default = window.__baCurrentLocale,
precision, default = 2,
show_minimal_price, default = false,
use_link_for_as_low_as, default = false,
minimal_price, default = null,
minimal_price_label, default = _('As low as'),
product_url, default = '',
special_price_label, default = _('Special Price'),
old_price_label, default = _('Was'),
Example:
<script>
import Price from '@modules/BA_Svelte/svelte/catalog/price.svelte';
let price = {
final_price: 79.99,
regular_price: 99.99,
has_special_price: true,
currency_code: 'GBP',
locale: 'en-GB',
special_price_label: _('Your price'), // make sure you pass translated labels
old_price_label: _('Instead of'),
};
</script>
<Price {...price} />The component is for rendering only. Decide what the price data should be in Magento or in your owning feature module, then pass the normalized values in as props.
import { dispatchStorefrontMessage, STORE_MESSAGE_TYPES } from '@modules/BA_Svelte/js/lib/messages.js';Use messages.js for:
- client-side success, error, and info messages
- Magento message fragments returned from AJAX responses
It emits an event using window.dispatchEvent(), theres a mixin to get this working with Luma. If you are not using a standard magento theme, you may need to add a listener like window.addEventListener('svelte:message', (event) => {...}. As an example see view/frontend/web/js/magento-mixin/messages.js
dispatchStorefrontMessage(STORE_MESSAGE_TYPES.error, _('Please enter a valid email'));
dispatchStorefrontMessage(STORE_MESSAGE_TYPES.success, _('Quote shared'));import {
buildRestUrl,
buildStorefrontUrl,
requestMagentoJson,
} from '@modules/BA_Svelte/js/lib/magento.js';Use magento.js for:
- storefront URL construction
- store-scoped REST URLs
- generic JSON transport to Magento
Rules:
- prefer server-resolved URLs via props when the route is already known during render
- use
buildRestUrl()instead of feature-localrest/${storeCode}/...glue. You should only need to include the route configured inwebapi.xmlin your parameter. - use
requestMagentoJson()for transport concerns only, not feature-specific business rules
Todo:
- GraphQL helpers
Status: beta. Expect change.
import { customerSections } from '$lib/state';Use state.js for:
- customer sections
const customer = customerSections.get('customer');const sections = customerSections.getMany([
'customer',
'cart',
]);await customerSections.load([
'customer',
'cart',
]);await customerSections.reload(
'customer',
'cart',
);customerSections.set('customer', {
firstname: 'Sam',
});const unsubscribe = customerSections.subscribe(
'cart',
(_, cart) => {
console.log(cart);
}
);Cleanup:
unsubscribe();let customer = $state(
customerSections.get('customer')
);
$effect(() => {
return customerSections.subscribe(
'customer',
(_, data) => {
customer = data;
}
);
});Use the shared singleton:
import { customerSections } from '$lib/state';Only instantiate CustomerSections directly when testing or creating isolated instances.
Status: very alpha. Expect change.
import {
applyValidationRules,
createAjaxFormController,
redirectTo,
resolveFormElement,
validateForm,
} from '@modules/BA_Svelte/js/lib/forms.js';Use forms.js for:
- validation
- AJAX submit lifecycle
- loading and disabled states
- redirect behavior after submit
Rules:
validateForm()is the replacement for manual Magento validation bootstraps in Svelte-driven flowscreateAjaxFormController()is the main AJAX form API- the controller owns
aria-busy, deduplicated submits, and button state - redirects should go through
redirectTo()
Status: Cart behavior, Needs implementing.
The runtime bootstrap itself is internal, but these markup contracts are public:
<ba-collapsible><ba-accordion><ba-modal><ba-add-to-cart><ba-quantity-switch>
Example modal:
<ba-modal trigger=".open-size-guide">
<h2>Size guide</h2>
<button type="button" data-ba-modal-close>Close</button>
</ba-modal>Example add-to-cart wrapper:
<ba-add-to-cart>
<form data-role="tocart-form" action="/checkout/cart/add" method="post">
...
<button type="submit" class="action tocart primary">
<span>Add to Cart</span>
</button>
</form>
</ba-add-to-cart><ba-quantity-switch> is also a public contract. Its controller lives behind the runtime, so feature modules should use the element instead of importing quantity-switch internals.
When you are modernizing a module, these are the preferred replacements:
| Replace this | Use this BA_Svelte surface | Do not use |
|---|---|---|
customer-data wrappers |
state.js |
feature-local customer section adapters |
Magento_Ui/js/modal/modal |
Popup.svelte or <ba-modal> |
runtime modal internals |
Magento_Catalog/js/validate-product |
forms.js plus commerce.js |
feature-local validation bootstraps |
catalogAddToCart |
commerce.js or <ba-add-to-cart> |
feature-local add-to-cart wrappers - NEEDS IMPLEMENTING |
| repeated REST URL glue | magento.js |
feature-local URL helpers |
| feature-local JSON fetch wrappers | magento.js |
http.js from feature code |
| feature-local flash/server message plumbing | messages.js |
duplicate message fragment implementations |
Internal paths that should not appear in new feature code:
@modules/BA_Svelte/js/lib/runtime.js@modules/BA_Svelte/js/lib/runtime/*@modules/BA_Svelte/js/lib/http.js@modules/BA_Svelte/js/lib/url.js
The default popup direction is native.
Use:
@modules/BA_Svelte/svelte/popup.svelte<ba-modal>
Example:
<script>
import Popup from '@modules/BA_Svelte/svelte/popup.svelte';
let popup = null;
</script>
<button type="button" onclick={() => popup?.open()}>
Open size guide
</button>
<Popup bind:this={popup} title="Size guide">
<p>Use your usual fit. This product runs true to size.</p>
</Popup>Exit criteria for a migrated popup:
- it does not rely on Magento modal widget lifecycle
- it can render with native dialog semantics
- it can move to
Popup.svelteor<ba-modal>without behavior loss
- Run
bin/magento setup:static-content:deploy BA_Sveltedetects deployed storefront roots containing BA_Svelte assets- For each deployed root, it runs
npm run buildinsideview/frontend/web/svelte-src
cd src/app/code/BA/Svelte/view/frontend/web/svelte-src
npm run buildIf you need to point at one deployed storefront root explicitly:
cd src/app/code/BA/Svelte/view/frontend/web/svelte-src
SCD_ROOT=/absolute/path/to/pub/static/<area>/<Vendor>/<Theme>/<Locale> npm run buildassets.phtmldeduplicates the shared CSS and JS bundle if multiple Svelte modules include it on the same page.SvelteBlockis for root mounts that renderBA_Svelte::root.phtmland nested pluggable components inside root containers.SvelteLinkis for sorted link collections.- Use
.svelte-rootblocks or the documented custom elements for new work. Do not build feature modules against runtime internals.