Documentation Index Fetch the complete documentation index at: https://mintlify.com/medusajs/medusa/llms.txt
Use this file to discover all available pages before exploring further.
Widgets are reusable UI components that can be injected into specific zones throughout the admin dashboard, allowing you to extend existing pages without modifying core code.
Overview
Widgets enable you to:
Add custom content to existing pages
Display additional information alongside core features
Integrate third-party services into the admin
Create reusable UI components across multiple pages
Create a widget by defining a React component and specifying where it should appear:
Create Widget File
src/admin/widgets/order-tracking/index.tsx
Define the Widget Component
// src/admin/widgets/order-tracking/index.tsx
import { Container , Heading , Text } from "@medusajs/ui"
const OrderTrackingWidget = () => {
return (
< Container className = "divide-y p-0" >
< div className = "flex items-center justify-between px-6 py-4" >
< Heading level = "h2" > Order Tracking </ Heading >
</ div >
< div className = "px-6 py-4" >
< Text > Custom tracking information will appear here </ Text >
</ div >
</ Container >
)
}
export default OrderTrackingWidget
Configure Injection Zone
import { defineWidgetConfig } from "@medusajs/admin-sdk"
export const config = defineWidgetConfig ({
zone: "order.details.after"
})
export default OrderTrackingWidget
The widget will automatically appear on all order detail pages, after the main content.
The defineWidgetConfig function accepts:
interface WidgetConfig {
/**
* The injection zone(s) where the widget should appear
*/
zone : InjectionZone | InjectionZone []
}
Multiple Zones
Inject a widget into multiple locations:
import { defineWidgetConfig } from "@medusajs/admin-sdk"
export const config = defineWidgetConfig ({
zone: [
"product.details.after" ,
"product.list.after"
]
})
Injection Zones
Injection zones are specific locations in the admin where widgets can be inserted. They follow the pattern:
{entity}.{page}.{position}
Available Zones
The full list of injection zones is defined in packages/admin/admin-shared/src/extensions/widgets/constants.ts:210:
Order Zones
"order.details.before"
"order.details.after"
"order.details.side.before"
"order.details.side.after"
"order.list.before"
"order.list.after"
Product Zones
"product.details.before"
"product.details.after"
"product.details.side.before"
"product.details.side.after"
"product.list.before"
"product.list.after"
Customer Zones
"customer.details.before"
"customer.details.after"
"customer.details.side.before"
"customer.details.side.after"
"customer.list.before"
"customer.list.after"
Other Entity Zones
// Product variants
"product_variant.details.before"
"product_variant.details.after"
"product_variant.details.side.before"
"product_variant.details.side.after"
// Collections
"product_collection.details.before"
"product_collection.details.after"
"product_collection.list.before"
"product_collection.list.after"
// Categories
"product_category.details.before"
"product_category.details.after"
"product_category.list.before"
"product_category.list.after"
// Promotions
"promotion.details.before"
"promotion.details.after"
"promotion.list.before"
"promotion.list.after"
// Price Lists
"price_list.details.before"
"price_list.details.after"
"price_list.list.before"
"price_list.list.after"
// And many more...
See the complete list in the source code.
Data Access
Route Parameters
Access route parameters to fetch relevant data:
import { useParams } from "react-router-dom"
import { useQuery } from "@tanstack/react-query"
import { Container , Heading } from "@medusajs/ui"
const ProductAnalyticsWidget = () => {
const { id } = useParams ()
const { data : analytics } = useQuery ({
queryKey: [ "product-analytics" , id ],
queryFn : async () => {
const response = await fetch ( `/admin/analytics/products/ ${ id } ` )
return response . json ()
}
})
return (
< Container >
< Heading level = "h3" > Product Analytics </ Heading >
< div className = "mt-4" >
< p > Views: { analytics ?. views || 0 } </ p >
< p > Conversion: { analytics ?. conversion || 0 } % </ p >
</ div >
</ Container >
)
}
export const config = defineWidgetConfig ({
zone: "product.details.side.after"
})
export default ProductAnalyticsWidget
Medusa SDK
Fetch data from Medusa using the Admin SDK:
import { useMedusa } from "@medusajs/dashboard"
import { useQuery } from "@tanstack/react-query"
import { Container , Badge } from "@medusajs/ui"
const OrderStatusWidget = () => {
const { client } = useMedusa ()
const { data : orders } = useQuery ({
queryKey: [ "recent-orders" ],
queryFn : async () => {
const response = await client . orders . list ({
limit: 5 ,
order: "-created_at"
})
return response . orders
}
})
return (
< Container >
< h3 className = "font-semibold mb-4" > Recent Orders </ h3 >
< div className = "space-y-2" >
{ orders ?. map (( order ) => (
< div key = { order . id } className = "flex justify-between items-center" >
< span className = "text-sm" > { order . display_id } </ span >
< Badge color = { order . status === "completed" ? "green" : "orange" } >
{ order . status }
</ Badge >
</ div >
)) }
</ div >
</ Container >
)
}
export const config = defineWidgetConfig ({
zone: "order.list.before"
})
export default OrderStatusWidget
Using Design System
Leverage Medusa UI components for consistent styling:
import {
Container ,
Heading ,
Text ,
Button ,
Badge ,
Table
} from "@medusajs/ui"
const StyledWidget = () => {
return (
< Container className = "divide-y p-0" >
< div className = "flex items-center justify-between px-6 py-4" >
< div >
< Heading level = "h2" > Widget Title </ Heading >
< Text size = "small" className = "text-ui-fg-subtle" >
Widget description
</ Text >
</ div >
< Button variant = "secondary" size = "small" >
Action
</ Button >
</ div >
< div className = "px-6 py-4" >
< Badge color = "green" > Active </ Badge >
</ div >
</ Container >
)
}
Responsive Layout
const ResponsiveWidget = () => {
return (
< Container >
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" >
< div className = "p-4 border rounded-lg" >
< h4 className = "font-semibold" > Metric 1 </ h4 >
< p className = "text-2xl mt-2" > 1,234 </ p >
</ div >
< div className = "p-4 border rounded-lg" >
< h4 className = "font-semibold" > Metric 2 </ h4 >
< p className = "text-2xl mt-2" > 5,678 </ p >
</ div >
< div className = "p-4 border rounded-lg" >
< h4 className = "font-semibold" > Metric 3 </ h4 >
< p className = "text-2xl mt-2" > 9,012 </ p >
</ div >
</ div >
</ Container >
)
}
Advanced Patterns
Conditional Rendering
Show widgets based on conditions:
import { useParams } from "react-router-dom"
import { useQuery } from "@tanstack/react-query"
const ConditionalWidget = () => {
const { id } = useParams ()
const { data : product } = useQuery ({
queryKey: [ "product" , id ],
queryFn : async () => {
const response = await fetch ( `/admin/products/ ${ id } ` )
return response . json ()
}
})
// Only show for digital products
if ( ! product ?. metadata ?. is_digital ) {
return null
}
return (
< Container >
< h3 > Digital Product Settings </ h3 >
{ /* Digital product specific content */ }
</ Container >
)
}
export const config = defineWidgetConfig ({
zone: "product.details.after"
})
export default ConditionalWidget
Create widgets with state and interactions:
import { useState } from "react"
import { Container , Button , Input } from "@medusajs/ui"
import { useMutation , useQueryClient } from "@tanstack/react-query"
const InteractiveWidget = () => {
const [ note , setNote ] = useState ( "" )
const queryClient = useQueryClient ()
const { mutate : saveNote } = useMutation ({
mutationFn : async ( text : string ) => {
await fetch ( "/admin/notes" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({ note: text })
})
},
onSuccess : () => {
queryClient . invalidateQueries ({ queryKey: [ "notes" ] })
setNote ( "" )
}
})
return (
< Container >
< h3 className = "font-semibold mb-4" > Quick Notes </ h3 >
< Input
value = { note }
onChange = { ( e ) => setNote ( e . target . value ) }
placeholder = "Add a note..."
/>
< Button
className = "mt-2"
onClick = { () => saveNote ( note ) }
disabled = { ! note }
>
Save Note
</ Button >
</ Container >
)
}
export const config = defineWidgetConfig ({
zone: "order.details.side.after"
})
export default InteractiveWidget
Integrate charts and visualizations:
import { Container , Heading } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { LineChart , Line , XAxis , YAxis , Tooltip } from "recharts"
const SalesChartWidget = () => {
const { data : salesData } = useQuery ({
queryKey: [ "sales-chart" ],
queryFn : async () => {
const response = await fetch ( "/admin/analytics/sales" )
return response . json ()
}
})
return (
< Container >
< Heading level = "h3" > Sales Trend </ Heading >
< div className = "mt-4" >
< LineChart width = { 600 } height = { 300 } data = { salesData } >
< XAxis dataKey = "date" />
< YAxis />
< Tooltip />
< Line type = "monotone" dataKey = "sales" stroke = "#8884d8" />
</ LineChart >
</ div >
</ Container >
)
}
export const config = defineWidgetConfig ({
zone: "order.list.after"
})
export default SalesChartWidget
Complete Example
Here’s a complete widget that displays product inventory across locations:
// src/admin/widgets/product-inventory/index.tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { useParams } from "react-router-dom"
import { useQuery } from "@tanstack/react-query"
import { Container , Heading , Table , Badge } from "@medusajs/ui"
type InventoryLevel = {
location_id : string
location_name : string
stocked_quantity : number
reserved_quantity : number
available_quantity : number
}
const ProductInventoryWidget = () => {
const { id } = useParams ()
const { data : inventory , isLoading } = useQuery ({
queryKey: [ "product-inventory" , id ],
queryFn : async () => {
const response = await fetch ( `/admin/products/ ${ id } /inventory-levels` )
return response . json () as Promise < InventoryLevel []>
}
})
if ( isLoading ) {
return (
< Container >
< p > Loading inventory... </ p >
</ Container >
)
}
return (
< Container className = "divide-y p-0" >
< div className = "flex items-center justify-between px-6 py-4" >
< Heading level = "h2" > Inventory by Location </ Heading >
</ div >
< div className = "px-6 py-4" >
< Table >
< Table.Header >
< Table.Row >
< Table.HeaderCell > Location </ Table.HeaderCell >
< Table.HeaderCell > In Stock </ Table.HeaderCell >
< Table.HeaderCell > Reserved </ Table.HeaderCell >
< Table.HeaderCell > Available </ Table.HeaderCell >
< Table.HeaderCell > Status </ Table.HeaderCell >
</ Table.Row >
</ Table.Header >
< Table.Body >
{ inventory ?. map (( level ) => (
< Table.Row key = { level . location_id } >
< Table.Cell > { level . location_name } </ Table.Cell >
< Table.Cell > { level . stocked_quantity } </ Table.Cell >
< Table.Cell > { level . reserved_quantity } </ Table.Cell >
< Table.Cell > { level . available_quantity } </ Table.Cell >
< Table.Cell >
< Badge color = { level . available_quantity > 0 ? "green" : "red" } >
{ level . available_quantity > 0 ? "In Stock" : "Out of Stock" }
</ Badge >
</ Table.Cell >
</ Table.Row >
)) }
</ Table.Body >
</ Table >
</ div >
</ Container >
)
}
export const config = defineWidgetConfig ({
zone: "product.details.after"
})
export default ProductInventoryWidget
Best Practices
Each widget should have a single, clear purpose. Create multiple small widgets rather than one large complex widget.
Always show loading indicators while fetching data to prevent layout shifts.
Wrap widgets in <Container> from @medusajs/ui for consistent spacing and styling.
Use React Query for efficient data fetching and caching. Avoid expensive computations in render.
Consider Widget Placement
Choose injection zones carefully. Use .side zones for supplementary info and .before/.after for primary content.
Next Steps