> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mercurjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Re-compose a built-in page

> Override a built-in panel page by dropping a route file at the same path and re-composing its compound component slots.

Every page in the admin and vendor panels is exported as a **compound component** — a root plus named slots like `Header`, `HeaderActions`, and `DataTable`. That means you never fork a page to change one part of it: you drop a `page.tsx` at the same route path, render the original page, and swap only the slot you care about. Everything you don't touch keeps its default behavior — data fetching, filters, pagination, i18n, all of it.

<Info>
  **This is not Medusa's widget system.** Medusa's admin lets you inject widgets into predefined zones around a page. Mercur takes a different approach: pages are React compound components you re-compose directly. There are no injection zones and no `defineWidgetConfig` — you get the actual page component and decide what renders inside it. See [Extending Panels](/rc/resources/customization/extending-panels) for the full comparison.
</Info>

## What you'll build

A vendor portal `/products` page with **Import** and **Export** buttons added next to the built-in Create button — while the table, search, filters, and everything else stay exactly as shipped. This is the same technique the official `product-import-export` block uses.

## Slot anatomy

Built-in pages are exported from the `/pages` subpath of each panel package:

```typescript theme={null}
import { ProductListPage } from "@mercurjs/vendor/pages"   // vendor portal
import { CustomerListPage } from "@mercurjs/admin/pages"   // admin panel
```

Each list page exposes the same family of slots:

| Slot                 | Renders                                                       |
| -------------------- | ------------------------------------------------------------- |
| `Table`              | The `Container` shell holding header + table                  |
| `Header`             | The title/actions row                                         |
| `HeaderTitle`        | Heading and subtitle                                          |
| `HeaderActions`      | The action button cluster                                     |
| `HeaderCreateButton` | The built-in Create button                                    |
| `DataTable`          | The wired data table — fetching, columns, filters, pagination |

Detail pages expose section slots instead (e.g. `CustomerDetailPage.Main`, `CustomerDetailPage.Sidebar`, `CustomerDetailPage.MainGeneralSection`).

## Override the page

<Steps>
  <Step title="Drop a route file at the same path">
    Create the file in your vendor app at the path that matches the built-in route. `/products` maps to `src/routes/products/page.tsx`:

    ```tsx apps/vendor/src/routes/products/page.tsx theme={null}
    import { Link } from "react-router-dom"
    import { Button } from "@medusajs/ui"
    import { ArrowDownTray, ArrowUpTray } from "@medusajs/icons"
    import { ProductListPage } from "@mercurjs/vendor/pages"

    export default function ProductsWithImportExport() {
      return (
        <ProductListPage>
          <ProductListPage.Table>
            <ProductListPage.Header>
              <ProductListPage.HeaderTitle />
              <ProductListPage.HeaderActions>
                <Button size="small" variant="secondary" asChild>
                  <Link to="import">
                    <ArrowUpTray />
                    Import
                  </Link>
                </Button>
                <Button size="small" variant="secondary" asChild>
                  <Link to="export">
                    <ArrowDownTray />
                    Export
                  </Link>
                </Button>
                <ProductListPage.HeaderCreateButton />
              </ProductListPage.HeaderActions>
            </ProductListPage.Header>
            <ProductListPage.DataTable />
          </ProductListPage.Table>
        </ProductListPage>
      )
    }
    ```
  </Step>

  <Step title="Reuse the slots you don't change">
    Note what happened in that file: `HeaderTitle`, `HeaderCreateButton`, and `DataTable` are the originals, rendered untouched. Only `HeaderActions` gained two buttons. You re-compose top-down — render the page, re-declare only the branch you're changing, and keep original slots for everything inside it.
  </Step>

  <Step title="Reload the panel">
    The dashboard SDK picks the file up automatically (adding or removing a route file triggers a full reload in dev). Because the path matches a built-in route, **your page replaces the built-in one** — no configuration, no registration.
  </Step>
</Steps>

<Note>
  When a custom route's path matches a built-in route, your page replaces the built-in one. Routes at new paths are appended instead. That one rule is the entire override mechanism.
</Note>

## How defaults work

If you render a compound page with **no children**, it renders its full default composition — so `<ProductListPage />` alone is identical to the built-in page. Every page root follows the pattern:

```tsx theme={null}
Children.count(children) > 0 ? children : <DefaultComposition />
```

## Verify

1. Run your project (`bun run dev`) and open the vendor portal.
2. Navigate to **Products** — the list should look identical to before, plus Import and Export buttons in the header.
3. Search, filter, and paginate the table — all built-in behavior must still work, since `DataTable` is untouched.
4. Delete your `src/routes/products/page.tsx` and reload — the built-in page comes back. Nothing was forked.

## FAQ

<AccordionGroup>
  <Accordion title="Where do I find which slots a page exposes?">
    Import the page from `@mercurjs/vendor/pages` or `@mercurjs/admin/pages` and let your editor's autocomplete list the attached members — every slot is a static property on the page component. The naming is consistent across pages: list pages expose `Table`/`Header`/`HeaderTitle`/`HeaderActions`/`HeaderCreateButton`/`DataTable`; detail pages expose `Main`/`Sidebar` plus one property per section.
  </Accordion>

  <Accordion title="Can I remove a built-in element, like the Create button?">
    Yes — re-composition is declarative. Anything you don't render doesn't appear: re-declare `HeaderActions` with only your own buttons and omit `HeaderCreateButton`.
  </Accordion>

  <Accordion title="What about changing the table columns?">
    `DataTable` is a single slot — you either keep it wholesale or replace it with your own table. For a different column set, replace the slot with your own component built on the shared `DataTable`/`useDataTable` primitives from the panel package.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Replace panel components" href="/rc/resources/tutorials/replace-panel-components">
    Swap global chrome like the sidebar or topbar instead of a single page.
  </Card>

  <Card title="Add a custom API route" href="/rc/resources/tutorials/custom-api-route">
    Back your custom page with a typed endpoint of your own.
  </Card>
</CardGroup>
