5.3. Custom Injection Zones
In this chapter, you'll learn how to add custom widget injection zones in a plugin.
What are Custom Injection Zones?#
The Medusa Admin has pre-defined injection zones where you insert widgets. When you build a plugin that adds custom admin pages, you can also register custom injection zones to allow other developers to inject widgets into your plugin's pages.
Adding custom injection zones in a plugin requires two steps:
- Expose the injection zones in your custom admin page using the
LayoutComposercomponent. - Register the injection zones in the
InjectionZoneRegistryinterface to enable type checking and autocompletion indefineWidgetConfig.
Step 1: Expose Injection Zones with LayoutComposer#
When you build a custom admin page in a plugin, use the LayoutComposer component from @medusajs/dashboard/components to structure the page's layout. This component also exposes injection zones based on the prefix you pass to its widgetsZonePrefix prop.
Single-Column Layout Example#
Single-column layouts are useful for listing pages or pages with a single main content area. For example:
1import { LayoutComposer } from "@medusajs/dashboard/components"2 3const BrandListPage = () => {4 // ...5 return (6 <LayoutComposer7 widgetsZonePrefix="brand.list"8 preferredLayoutId="core:single-column"9 sections={{10 main: (11 <BrandListSection />12 ),13 }}14 />15 )16}17 18export default BrandListPage
Based on the widgetsZonePrefix prop, the LayoutComposer in the example exposes the following injection zone:
brand.list: Widgets rendered in the main section of the brand list page.
Widgets injected into this zone appear inside the main section and can be reordered by admin users.
Refer to the LayoutComposer reference for more details on the component's props and layouts.
Two-Column Layout Example#
Two-column layouts are useful for details pages or pages with a main content area and a side column. For example:
1import { LayoutComposer } from "@medusajs/dashboard/components"2 3const BrandDetailsPage = () => {4 // ...5 return (6 <LayoutComposer7 widgetsZonePrefix="brand.details"8 preferredLayoutId="core:two-column"9 data={brand}10 sections={{11 main: (12 <>13 <GeneralSection brand={brand} />14 {/* ... */}15 </>16 ),17 }}18 />19 )20}21 22export default BrandDetailsPage
Based on the widgetsZonePrefix prop, the LayoutComposer in the example exposes the following injection zones:
brand.details: Widgets rendered in the main section of the brand details page.brand.details.side: Widgets rendered in the side section of the brand details page.
Widgets injected into these zones appear inside the respective sections and can be reordered by admin users. The data prop is passed to the widgets, allowing them to access the page's main data. In this example, widgets can access the brand object through their data prop.
Refer to the LayoutComposer reference for more details on the component's props and layouts.
Custom Zone Naming Convention#
Custom injection zones should follow the pattern {resource-name}.{page-context}.{position}, where:
resource-name: The name of the resource being accessed in the page. For example,brandin the example above.page-context: The page or section context. For example,detailsfor a details page orlistfor a list page.position: One ofbefore,after,side.before, orside.after.
This naming convention helps avoid conflicts between different plugins and makes zones easier to identify.
Step 2: Register Injection Zones for Type Checking#
Once your page exposes injection zones, register them in the InjectionZoneRegistry interface. This enables type checking and autocompletion for the zones when other developers create widgets using defineWidgetConfig.
To register the injection zones, augment the InjectionZoneRegistry interface through declaration merging. Create an index.d.ts file in your plugin's root directory with the following content:
Then, include the declaration file in your plugin's package.json:
Use Custom Injection Zones in Widgets#
Once registered, users using the plugin can inject widgets into the custom zones using defineWidgetConfig. For example:
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2 3const BrandListWidget = () => {4 return (5 <Container className="divide-y p-0">6 <div className="flex items-center justify-between px-6 py-4">7 <Heading level="h2">8 Brand Widget9 </Heading>10 </div>11 </Container>12 )13}14 15export const config = defineWidgetConfig({16 zone: "brand.list.before",17})18 19export default BrandListWidget
In this example, the widget is injected into the brand.list.before zone, so it's rendered before the main content of the brand list page you added to your plugin.
If you don't see brand.list.before in the autocompletion of the zone property, make sure your src/admin/tsconfig.json has the following in its include array:
Use Data Prop in Widgets#
If you pass a data prop to the LayoutComposer, widgets can access it through their props.
For example, if you pass a brand object as data to the LayoutComposer, you can access it in your widget like this:
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Container, Heading } from "@medusajs/ui"3import { 4 DetailWidgetProps,5} from "@medusajs/framework/types"6 7type BrandDetailsData = {8 id: string9 title: string10 // other brand properties...11}12 13const BrandWidget = ({ 14 data,15}: DetailWidgetProps<BrandDetailsData>) => {16 return (17 <Container className="divide-y p-0">18 <div className="flex items-center justify-between px-6 py-4">19 <Heading level="h2">20 Brand Widget {data.title}21 </Heading>22 </div>23 </Container>24 )25}26 27export const config = defineWidgetConfig({28 zone: "brand.details.before",29})30 31export default BrandWidget
In this example, the BrandWidget is injected into the brand.details.before zone, which is part of the brand details page you added to your plugin. Since you passed the brand object as data to the LayoutComposer, developers can access it in the widget through the data prop.