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
Polaris web components
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
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
Anchor to Updated propertiesUpdated 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. |
Anchor to Replace open state with commandsReplace open state with commands
A button can show or hide a modal by referencing its ID:
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.
Anchor to Test the migrationTest 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.
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.