Skip to main content

Migrate Modal from Polaris React

Replace Polaris React Modal with s-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 for contextual content and s-menu for a compact list of actions.


Anchor to Migrate a confirmation dialogMigrate 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

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>
</>
);
}
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>
</>
);
}

Preview


Polaris ReactPolaris web componentsMigration notes
openA 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.
onCloseThe hide event, exposed as onHide in JSXKeep a callback only when app state needs to react to every close path.
titleheadingPass the dialog heading as a string.
primaryActionOne s-button in slot="primary-action"Move content and behavior from the descriptor to the button. Use variant="primary".
secondaryActionss-button elements in slot="secondary-actions"Render each action as a button.
destructive on an actiontone="critical"Keep destructive tone on the relevant button.
Action loadingloading on the slotted s-buttonKeep the dialog open while the operation is pending.
Action disableddisabled on the slotted s-buttonPreserve the condition that prevents the action.
loadingLocal loading content and disabled or loading actionss-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.
limitHeightRemoves-modal owns its viewport constraints and scrolling. Test long content and narrow viewports instead of recreating the old height cap.
noScrollRemove or redesign the contents-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.

Anchor to Replace open state with commandsReplace open state with commands

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

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.


Anchor to Compose the dialog contentCompose 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.


Anchor to Preserve close behaviorPreserve 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.


  • 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.

Anchor to Remove Polaris ReactRemove 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.



Was this page helpful?