Skip to main content
The vendor store-setup / onboarding surface is a widget zone (seller.setup) that renders the full seller object as its data. That makes it the perfect seam for an onboarding checklist: derive completion from the seller you’re handed, show what’s left, and link each step to the page that completes it — all from a single widget file, with nothing fetched and nothing forked.
This is the pattern the built-in store-setup card uses. The shipped “Complete store profile” card is itself a seller.setup widget that reads the seller and renders a progress list. This tutorial rebuilds it so you can shape your own onboarding steps.

What you’ll build

A collapsible progress card on the store-setup surface that checks four parts of the seller profile (details, address, company, payment), draws a progress bar, and routes the vendor to the settings page for each incomplete step. When every step is done, the card removes itself.

Register the typed targets (once)

Widget zones are typed ids the vendor panel generates from its own pages and ships as @mercurjs/vendor/extension-targets. Reference them once so seller.setup autocompletes and a wrong zone fails tsc:
apps/vendor/src/extension-targets.d.ts
/// <reference types="@mercurjs/vendor/extension-targets" />

Build the checklist widget

1

Derive the steps from the seller

The data prop is the full SellerDTO. Compute each step’s completed flag from it — no request needed. Give every step a path to the settings page that finishes it:
apps/vendor/src/widgets/store-setup-checklist.tsx
import "@mercurjs/vendor/extension-targets"
import { SellerDTO } from "@mercurjs/types"

type ProfileStep = {
  key: string
  label: string
  completed: boolean
  path: string
}

function getProfileSteps(seller: SellerDTO): ProfileStep[] {
  const hasStoreDetails = !!(seller.name && seller.email && seller.description)
  const hasAddress = !!(
    seller.address?.address_1 &&
    seller.address?.city &&
    seller.address?.country_code
  )
  const hasCompanyDetails = !!seller.professional_details?.corporate_name
  const hasPaymentDetails = !!(
    seller.payment_details?.holder_name &&
    seller.payment_details?.country_code
  )

  return [
    { key: "store_details", label: "Add store details", completed: hasStoreDetails, path: "/settings/store/edit" },
    { key: "address", label: "Add address", completed: hasAddress, path: "/settings/store/address" },
    { key: "company_details", label: "Add company details", completed: hasCompanyDetails, path: "/settings/store/professional-details" },
    { key: "payment_details", label: "Add payment details", completed: hasPaymentDetails, path: "/settings/store/payment-details" },
  ]
}
2

Render the collapsible progress card

Use @medusajs/ui primitives and @medusajs/icons only. Draw a progress bar from the completed ratio, list each step with a status icon, and navigate on click. Return null when everything is done so the card disappears:
apps/vendor/src/widgets/store-setup-checklist.tsx
import { useMemo, useState } from "react"
import { useNavigate } from "react-router-dom"
import { Container, Text, clx } from "@medusajs/ui"
import { CheckCircleSolid, TriangleDownMini, CircleDottedLine } from "@medusajs/icons"
import { Collapsible as RadixCollapsible } from "radix-ui"

const StoreSetupChecklist = ({ data: seller }: { data?: SellerDTO }) => {
  const navigate = useNavigate()
  const [open, setOpen] = useState(true)

  const steps = useMemo(
    () => (seller ? getProfileSteps(seller) : []),
    [seller]
  )

  if (!seller) {
    return null
  }

  const completedCount = steps.filter((s) => s.completed).length
  const totalCount = steps.length
  const progressPercent = (completedCount / totalCount) * 100

  if (completedCount === totalCount) {
    return null
  }

  return (
    <RadixCollapsible.Root open={open} onOpenChange={setOpen}>
      <Container className="overflow-hidden p-0">
        <div
          className="h-1 bg-ui-tag-green-icon transition-all duration-500"
          style={{ width: `${progressPercent}%` }}
        />
        <div className="p-6">
          <RadixCollapsible.Trigger asChild>
            <button className="flex w-full items-center justify-between">
              <Text size="large" weight="plus" leading="compact">
                Complete store profile
              </Text>
              <TriangleDownMini
                className={clx(
                  "text-ui-fg-muted transition-transform duration-200",
                  !open && "-rotate-90"
                )}
              />
            </button>
          </RadixCollapsible.Trigger>

          <RadixCollapsible.Content>
            <div className="mt-4 flex flex-col gap-y-3">
              {steps.map((step) => (
                <button
                  key={step.key}
                  className="flex items-center gap-x-3 text-left"
                  onClick={() => !step.completed && navigate(step.path)}
                  disabled={step.completed}
                >
                  {step.completed ? (
                    <CheckCircleSolid className="text-ui-tag-green-icon shrink-0" />
                  ) : (
                    <CircleDottedLine className="text-ui-fg-muted shrink-0" />
                  )}
                  <Text size="small" leading="compact" className="text-ui-fg-base">
                    {step.label}
                  </Text>
                </button>
              ))}
            </div>
          </RadixCollapsible.Content>
        </div>
      </Container>
    </RadixCollapsible.Root>
  )
}
3

Attach it to the store-setup zone

Export the component as the default and a config with a seller.setup zone. before renders it above the built-in card; use after to place it below:
apps/vendor/src/widgets/store-setup-checklist.tsx
import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"

export const config = defineWidgetConfig({
  zone: "seller.setup.before",
})

export default StoreSetupChecklist

Where it renders

seller.setup is hosted in two places, both passing the same seller as data:
HostWhen it shows
The vendor shell (above the page outlet)On top-level routes — the dashboard “home” onboarding banner
The store settings detail pageAlways, above the store status banner
A single widget file covers both. Multiple seller.setup.before / .after widgets stack in registration order, so your checklist and the built-in card can coexist — or hide the built-in one by rendering your own and letting theirs complete.
No data fetching. Because the zone hands you the resolved seller, the widget is pure render — you never call the SDK. To collect and persist a new onboarding value (e.g. a Tax ID) rather than just link to existing pages, see Extend the onboarding flow, which carries a value through additional_data to a workflow hook.

Next steps

Extend the onboarding flow

Collect a new value and persist it through a workflow hook.

Add a widget

The full widget model and the published zone registry.