Skip to main content
A widget is a React component attached to a named zone on a built-in page. You drop one file under src/widgets/, and the SDK renders it at that zone while the rest of the page stays exactly as shipped, including its data fetching, filters, and pagination. This is the lightest way to add UI to a page you don’t own. Reach for it first when you want to add something to an existing screen.
A widget is additive, not a replacement. Unlike a drop-in route, which owns the whole page, a widget layers your component onto the built-in page at a documented zone.

What you’ll build

A tip banner above the vendor product list, rendered from a single file, with the list itself untouched.

Register the typed targets

Widget zones are typed ids that the vendor panel generates from its own pages and ships as @mercurjs/vendor/extension-targets. Register them once so the ids resolve everywhere, with a single ambient reference in your app’s src.
apps/vendor/src/extension-targets.d.ts
Projects from create-mercur-app already ship this file. With it present, an invalid zone fails tsc instead of silently doing nothing.

Add the widget

1

Create the widget file

Drop a file under src/widgets/. Export the component as the default and a config built with defineWidgetConfig. The zone names where it renders.
apps/vendor/src/widgets/product-list-banner.tsx
2

Understand the zone id

A zone id reads <domain>.<view>.<placement>. The last segment is the placement.Multiple before or after widgets on the same zone stack in registration order.
3

Reload the panel

Start the project and open the vendor portal. Widget files hot-reload. The banner appears above the product list, and the table below it works exactly as before.
Terminal

Available zones

These zones are mounted today in the vendor portal. The full, valid set is typed as WidgetZoneId and generated into @mercurjs/vendor/extension-targets from the panel’s own zone hosts. Let your editor autocomplete zone: to see every option. A zone no page renders can’t be targeted and won’t type-check.

Verify

  1. The tip banner renders above the product list.
  2. Search, filter, and paginate the list. All built-in behavior still works.
  3. Change the zone to product.list.after and reload. The banner moves below the list.
  4. Set zone: "not.a.zone". tsc (bun run lint) fails with a “not assignable to WidgetZoneId” error.
  5. Delete the file. The banner disappears, and nothing else changes.

FAQ

Yes. zone accepts an array (zone: ["product.list.before", "login.after.before"]), and the same component renders at each.
Yes. A block can include src/widgets/ files in its vendor_ui or admin_ui entry, and they’re aggregated just like the host app’s. Installing the block adds the widget with no wiring.
The zone set is per panel and generated from each panel’s pages. Today the mounted zones live in the vendor portal (product.list.*, login.*). The admin panel exposes navigation and product custom fields. Check @mercurjs/admin/extension-targets for its current zones.

Next steps

Extend forms and tables

Add validated fields and columns with defineCustomFieldsConfig.

Extend the onboarding flow

Add a store-setup field and persist it through a workflow hook.