5.3. Custom Injection Zones

In this chapter, you'll learn how to add custom widget injection zones in a plugin.

Note: Custom injection zones are available since Medusa v2.16.0.

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:

  1. Expose the injection zones in your custom admin page using the LayoutComposer component.
  2. Register the injection zones in the InjectionZoneRegistry interface to enable type checking and autocompletion in defineWidgetConfig.

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:

src/admin/routes/custom/[id]/page.tsx
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:

src/admin/routes/custom/[id]/page.tsx
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, brand in the example above.
  • page-context: The page or section context. For example, details for a details page or list for a list page.
  • position: One of before, after, side.before, or side.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:

index.d.ts
1declare module "@medusajs/admin-shared" {2  interface InjectionZoneRegistry {3    "brand.details.before": true4    "brand.details.after": true5    "brand.details.side.before": true6    "brand.details.side.after": true7  }8}

Then, include the declaration file in your plugin's package.json:

package.json
1{2  "files": [3    ".medusa/server",4    "index.d.ts"5  ],6  "exports": {7    ".": {8      "types": "./index.d.ts"9    }10  }11}

Use Custom Injection Zones in Widgets#

Once registered, users using the plugin can inject widgets into the custom zones using defineWidgetConfig. For example:

src/admin/widgets/draft-order-widget.tsx
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:

src/admin/tsconfig.json
1"include": [2  ".",3  "../../.medusa/types/plugin-augmentations.d.ts"4]
Note: When you build your application, you'll see a warning that this zone is unknown. This is only a precaution since the system doesn't recognize the custom zone. Make sure to use the correct zone name.

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:

src/admin/widgets/draft-order-widget.tsx
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.

Was this chapter helpful?
Ask Bloom
For assistance in your development, use Claude Code Plugins or Medusa MCP server in Cursor, VSCode, etc...FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break