---
title: Migrate Modal from Polaris React
description: >-
  Replace the Polaris React Modal component with s-modal, slotted actions, and
  command-based controls.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/modal
  md: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/modal.md
api_name: app-home
---

# Migrate Modal from Polaris React

Replace Polaris React `Modal` with [`s-modal`](https://shopify.dev/docs/api/app-home/web-components/overlays/modal). The migrated dialog keeps its content as children, but replaces the `open` prop and action descriptor objects with commands and slotted `s-button` elements.

Use a modal for a focused task that interrupts the current workflow. Use [`s-popover`](https://shopify.dev/docs/api/app-home/web-components/overlays/popover) for contextual content and [`s-menu`](https://shopify.dev/docs/api/app-home/web-components/actions/menu) for a compact list of actions.

***

## Migrate a confirmation dialog

The following example closes the dialog only after deletion succeeds. If `onDelete` rejects, then the dialog remains open so that the app can display an error and let the user retry.

## Migrating a deletion confirmation

##### Polaris web components

```tsx
import {useState} from 'react';

export function DeleteProductDialog({
  onDelete,
}: {
  onDelete: () => Promise<void>;
}) {
  const [pending, setPending] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleDelete() {
    if (pending) return;

    setPending(true);
    setError(null);
    try {
      await onDelete();
      await shopify.modal.hide('delete-product-modal');
    } catch {
      setError("The product couldn't be deleted. Try again.");
    } finally {
      setPending(false);
    }
  }

  return (
    <>
      <s-button commandFor="delete-product-modal" tone="critical">
        Delete product
      </s-button>
      <s-modal id="delete-product-modal" heading="Delete product">
        <s-stack gap="small">
          <s-paragraph>This can't be undone.</s-paragraph>
          {error && <s-paragraph>{error}</s-paragraph>}
        </s-stack>
        <s-button
          slot="primary-action"
          variant="primary"
          tone="critical"
          loading={pending}
          onClick={handleDelete}
        >
          Delete
        </s-button>
        <s-button
          slot="secondary-actions"
          disabled={pending}
          commandFor="delete-product-modal"
          command="--hide"
        >
          Cancel
        </s-button>
      </s-modal>
    </>
  );
}
```

##### Polaris React

```tsx
import {Button, Modal, Text} from '@shopify/polaris';
import {useState} from 'react';

export function DeleteProductDialog({
  onDelete,
}: {
  onDelete: () => Promise<void>;
}) {
  const [open, setOpen] = useState(false);

  async function handleDelete() {
    await onDelete();
    setOpen(false);
  }

  return (
    <>
      <Button destructive onClick={() => setOpen(true)}>
        Delete product
      </Button>
      <Modal
        open={open}
        onClose={() => setOpen(false)}
        title="Delete product"
        primaryAction={{
          content: 'Delete',
          destructive: true,
          onAction: handleDelete,
        }}
        secondaryActions={[
          {content: 'Cancel', onAction: () => setOpen(false)},
        ]}
      >
        <Modal.Section>
          <Text as="p">This can't be undone.</Text>
        </Modal.Section>
      </Modal>
    </>
  );
}
```

***

## Updated properties

| Polaris React | Polaris web components | Migration notes |
| - | - | - |
| `open` | A trigger with `commandFor`, or `shopify.modal.show(id)` and `shopify.modal.hide(id)` | Remove React state when it exists only to show or hide the dialog. |
| `onClose` | The `hide` event, exposed as `onHide` in JSX | Keep a callback only when app state needs to react to every close path. |
| `title` | `heading` | Pass the dialog heading as a string. |
| `primaryAction` | One `s-button` in `slot="primary-action"` | Move content and behavior from the descriptor to the button. Use `variant="primary"`. |
| `secondaryActions` | `s-button` elements in `slot="secondary-actions"` | Render each action as a button. |
| `destructive` on an action | `tone="critical"` | Keep destructive tone on the relevant button. |
| Action `loading` | `loading` on the slotted `s-button` | Keep the dialog open while the operation is pending. |
| Action `disabled` | `disabled` on the slotted `s-button` | Preserve the condition that prevents the action. |
| `loading` | Local loading content and disabled or loading actions | `s-modal` has no modal-wide loading property. Keep the heading and dialog context visible, replace the affected content with a labelled `s-spinner` when necessary, and prevent duplicate actions. |
| `limitHeight` | Remove | `s-modal` owns its viewport constraints and scrolling. Test long content and narrow viewports instead of recreating the old height cap. |
| `noScroll` | Remove or redesign the content | `s-modal` doesn't expose a switch that disables its scrolling. Keep content in normal flow; use a focused custom layout only when the task genuinely requires fixed regions. |

***

## Replace open state with commands

A button can show or hide a modal by referencing its ID:

```tsx
function ProductModal() {
  return (
    <>
      <s-button commandFor="product-modal" command="--show">
        Open product
      </s-button>
      <s-modal id="product-modal" heading="Product details">
        <s-paragraph>Product details go here.</s-paragraph>
        <s-button
          slot="primary-action"
          variant="primary"
          commandFor="product-modal"
          command="--hide"
        >
          Done
        </s-button>
      </s-modal>
    </>
  );
}
```

The `command` value is optional when a trigger should toggle the modal. Use explicit `--show` and `--hide` commands when the action has only one valid outcome.

Use `shopify.modal.show(id)`, `shopify.modal.hide(id)`, or `shopify.modal.toggle(id)` when asynchronous app logic controls visibility. Don't recreate `open` state and an effect solely to call these methods.

***

## Compose the dialog content

Replace `Modal.Section` with semantic content inside `s-modal`, such as `s-section`, `s-stack`, or form fields. The modal owns its outer padding. Add a section only when the content needs a section heading or distinct structure.

Keep validation errors inside the dialog. For an asynchronous primary action, set loading state on its button, await the operation, and hide the modal only after success. On failure, remove loading state, keep the modal open, and display a recoverable error.

When the old modal used its top-level `loading` property, decide what is actually pending. For a whole-dialog fetch, keep the modal heading mounted and show an `s-spinner` whose `accessibilityLabel` names the content being loaded. For a submission, keep the current content visible and put `loading` on the submitting action. Don't replace a recoverable form with an unexplained spinner.

***

## Preserve close behavior

Test every way that the dialog can close, including its secondary action, the dismiss control, the Escape key, and programmatic dismissal. Use the `hide` or `afterhide` event when app state must update regardless of how the dialog closes.

Don't rely on the trigger's click handler to reset dialog state. That handler doesn't run when the user closes the dialog another way.

***

## Test the migration

* Open and close the dialog with a pointer and keyboard.
* Verify focus moves into the dialog and returns to the trigger after dismissal.
* Verify primary and secondary actions preserve their loading, disabled, and critical states.
* Test success and error paths for asynchronous actions.
* Confirm that validation errors remain visible and associated with their fields.

***

## Remove Polaris React

After every `Modal` call site is migrated, remove the `Modal` import and any open state used only to control it. Remove `@shopify/polaris` only after no other route in scope imports it.

***

## Related guidance

* [Modal component](https://shopify.dev/docs/api/app-home/web-components/overlays/modal)
* [Modal API](https://shopify.dev/docs/api/app-home/apis/user-interface-and-interactions/modal-api)
* [Popover component](https://shopify.dev/docs/api/app-home/web-components/overlays/popover)

***
