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
- The tip banner renders above the product list.
- Search, filter, and paginate the list. All built-in behavior still works.
- Change the zone to
product.list.afterand reload. The banner moves below the list. - Set
zone: "not.a.zone".tsc(bun run lint) fails with a “not assignable toWidgetZoneId” error. - Delete the file. The banner disappears, and nothing else changes.
FAQ
Can a widget target more than one zone?
Can a widget target more than one zone?
Yes.
zone accepts an array (zone: ["product.list.before", "login.after.before"]), and the same component renders at each.Can a block ship widgets?
Can a block ship widgets?
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.Does the admin panel have widget zones too?
Does the admin panel have widget zones too?
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.