> ## 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.

# Custom Widgets

> Learn how to create and inject custom widgets into the Medusa admin dashboard

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

## Creating a Widget

### Basic Widget

Create a widget by defining a React component and specifying where it should appear:

<Steps>
  <Step title="Create Widget File">
    ```bash theme={null}
    src/admin/widgets/order-tracking/index.tsx
    ```
  </Step>

  <Step title="Define the Widget Component">
    ```tsx theme={null}
    // 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
    ```
  </Step>

  <Step title="Configure Injection Zone">
    ```tsx theme={null}
    import { defineWidgetConfig } from "@medusajs/admin-sdk"

    export const config = defineWidgetConfig({
      zone: "order.details.after"
    })

    export default OrderTrackingWidget
    ```
  </Step>
</Steps>

The widget will automatically appear on all order detail pages, after the main content.

## Widget Configuration

The `defineWidgetConfig` function accepts:

```tsx theme={null}
interface WidgetConfig {
  /**
   * The injection zone(s) where the widget should appear
   */
  zone: InjectionZone | InjectionZone[]
}
```

### Multiple Zones

Inject a widget into multiple locations:

```tsx theme={null}
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

```tsx theme={null}
"order.details.before"
"order.details.after"
"order.details.side.before"
"order.details.side.after"
"order.list.before"
"order.list.after"
```

#### Product Zones

```tsx theme={null}
"product.details.before"
"product.details.after"
"product.details.side.before"
"product.details.side.after"
"product.list.before"
"product.list.after"
```

#### Customer Zones

```tsx theme={null}
"customer.details.before"
"customer.details.after"
"customer.details.side.before"
"customer.details.side.after"
"customer.list.before"
"customer.list.after"
```

#### Other Entity Zones

```tsx theme={null}
// 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:

```tsx theme={null}
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:

```tsx theme={null}
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
```

## Styling Widgets

### Using Design System

Leverage Medusa UI components for consistent styling:

```tsx theme={null}
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

```tsx theme={null}
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:

```tsx theme={null}
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
```

### Interactive Widgets

Create widgets with state and interactions:

```tsx theme={null}
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
```

### Chart Widget

Integrate charts and visualizations:

```tsx theme={null}
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:

```tsx theme={null}
// 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

<AccordionGroup>
  <Accordion title="Keep Widgets Focused">
    Each widget should have a single, clear purpose. Create multiple small widgets rather than one large complex widget.
  </Accordion>

  <Accordion title="Handle Loading States">
    Always show loading indicators while fetching data to prevent layout shifts.
  </Accordion>

  <Accordion title="Use Container Components">
    Wrap widgets in `<Container>` from `@medusajs/ui` for consistent spacing and styling.
  </Accordion>

  <Accordion title="Optimize Performance">
    Use React Query for efficient data fetching and caching. Avoid expensive computations in render.
  </Accordion>

  <Accordion title="Consider Widget Placement">
    Choose injection zones carefully. Use `.side` zones for supplementary info and `.before/.after` for primary content.
  </Accordion>
</AccordionGroup>

## Next Steps

* Learn about [custom routes](/admin/ui-routes)
* Explore [dashboard customization](/admin/customization)
* Review the [JavaScript SDK](/sdk/overview) for API integration
