Skip to main content
The Admin and Vendor panels share one design system. You do three things with it: style with @medusajs/ui, extend built-in screens with custom fields, and add new pages. Each has an established shape and a set of correct imports. This page is that short list. For the full reference, see the panel extensions reference.

Use @medusajs/ui, and only it

Components come from @medusajs/ui, icons from @medusajs/icons, and colours, spacing, and type from Medusa UI tokens (text-ui-fg-*, bg-ui-bg-*, border-ui-border-*). Never use hex, rgb(), or text-gray-500.
Never introduce a second UI library, and never restyle Medusa UI components with custom CSS. Build on the primitives. Do not work around them.
A section is a Container with the standard shell: a divided card with a header row.

Extend built-in screens with custom fields

The primary way to customise an existing entity’s screens (product, order, customer) is a custom-fields config: one file per model that contributes form fields, table columns, and read-only section fields. See Custom fields for the full backend and frontend loop. This section covers the frontend surface with the right imports. You need two imports, and each lives in a different package.
defineCustomFieldsConfig is build-time config (SDK, zod-free). createFormHelper is the runtime form surface (dashboard-shared). Do not cross them over.

Add form fields (edit / create)

Contribute inputs into a built-in form zone. Values submit under additional_data.
apps/vendor/src/custom-fields/product.tsx

Change the list table

Add or override a column, and add bulk actions, on the model’s built-in list.

Read-only fields in detail sections

Use displays to add read-only rows into an existing detail-page section, keyed by id. An unknown id adds a row, a built-in id replaces one, and component: null hides one. A read-only field can render a StatusBadge, and a section action can trigger a status change through a mutation.
apps/vendor/src/custom-fields/product.tsx
Read-only displays are the idiomatic way to expose an entity’s state, such as an approval flag, a moderation status, or an internal tag, and to act on it without rebuilding the detail page. The mutation still goes through the typed SDK and rides additional_data into a workflow hook, never a direct write.

Add a new page

A brand-new screen is one file. Drop a page.tsx under the host app’s src/routes/. The SDK registers the route from the file path and builds the sidebar entry from an exported config.
apps/vendor/src/routes/reviews/page.tsx
Correct imports for a page: UI from @medusajs/ui, icons from @medusajs/icons, and the RouteConfig type from @mercurjs/dashboard-sdk. Dynamic segments use brackets: src/routes/reviews/[id]/page.tsx maps to /reviews/:id. See Extending panels.

Compose a full page: layout, table, sections, edit

For a real screen you assemble the same primitives the built-in pages use. They are all re-exported from @mercurjs/dashboard-shared, so you import from one place instead of Medusa internals.
Import these primitives from @mercurjs/dashboard-shared, not from deep Medusa dashboard paths like ../../../components/table/data-table. The shared package is the public, stable surface. Relative Medusa-internal imports are not available to consumer apps and break on upgrade.

Layout and list table

Pick a layout: SingleColumnPage for lists and simple pages, TwoColumnPage for a detail with a sidebar. Mount a DataTable inside the standard section shell. Build columns with createColumnHelper, wire the table with useDataTable, use page size 20, and pass keepPreviousData for smooth pagination.
apps/vendor/src/routes/reviews/page.tsx

General section (label / value rows)

On a detail page, a “general” section is a Container header row plus SectionRow label and value pairs. This is the canonical way Medusa renders read-only entity data.
For a detail page with a sidebar, wrap sections in TwoColumnPage and place them under TwoColumnPage.Main and TwoColumnPage.Sidebar, each stacked with gap-y-3.

Edit page (drawer)

Quick edits live in a routed RouteDrawer with Form (React Hook Form plus Zod). Gate the form until the entity has loaded, and use useRouteModal().handleSuccess() to close on save.
apps/vendor/src/routes/reviews/[id]/edit/page.tsx

Data only through the typed SDK

Never call fetch directly from a page. All HTTP goes through the typed SDK (sdk.admin.* in the admin panel, sdk.vendor.* in the vendor panel), wrapped in TanStack Query hooks.
src/hooks/api/reviews.tsx
Invalidate lists(), details(), and detail(id) in mutations. Throw on isError so the route ErrorBoundary catches it. Show a Skeleton while loading.

Checklist for panel work

  • UI primitives: built only from @medusajs/ui and @medusajs/icons, Medusa UI tokens only, no custom CSS.
  • Extending a screen: a defineCustomFieldsConfig file (@mercurjs/dashboard-sdk) with createFormHelper (@mercurjs/dashboard-shared). Forms submit under additional_data.
  • Read-only state: status and flags surfaced via displays. Changes go through the typed SDK and a workflow hook, not a direct write.
  • New screen: a page.tsx under src/routes/ with a typed RouteConfig. Compose it from SingleColumnPage or TwoColumnPage, DataTable, SectionRow, and RouteDrawer, all imported from @mercurjs/dashboard-shared, never Medusa-internal paths.
  • Data: via sdk.admin.* or sdk.vendor.* in TanStack Query hooks. No raw fetch. Mutations invalidate the right keys.
  • Strings and test ids: every visible string translated, every interactive element has a data-testid.

Next steps

Panel extensions reference

The full reference for custom fields, widgets, and new pages.

Custom fields

The full backend and frontend loop, including the workflow hook.