@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.
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 formzone. 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
Usedisplays 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
Add a new page
A brand-new screen is one file. Drop apage.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.
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 aContainer header row plus SectionRow label and value pairs. This is the canonical way Medusa renders read-only entity data.
TwoColumnPage and place them under TwoColumnPage.Main and TwoColumnPage.Sidebar, each stacked with gap-y-3.
Edit page (drawer)
Quick edits live in a routedRouteDrawer 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
src/hooks/api/reviews.tsx
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/uiand@medusajs/icons, Medusa UI tokens only, no custom CSS. - Extending a screen: a
defineCustomFieldsConfigfile (@mercurjs/dashboard-sdk) withcreateFormHelper(@mercurjs/dashboard-shared). Forms submit underadditional_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.tsxundersrc/routes/with a typedRouteConfig. Compose it fromSingleColumnPageorTwoColumnPage,DataTable,SectionRow, andRouteDrawer, all imported from@mercurjs/dashboard-shared, never Medusa-internal paths. - Data: via
sdk.admin.*orsdk.vendor.*in TanStack Query hooks. No rawfetch. 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.