Skip to main content

Migrate UnstableBulkActions from Polaris React

Polaris web components don't provide a standalone bulk-actions toolbar. Replace Polaris React UnstableBulkActions from @shopify/polaris with selection checkboxes and action controls inside the same s-table or resource list that owns the resources.

The app owns selected resource IDs, page-selection calculations, cross-page scope, mutations, results, and clearing selection. Keep that state with filtering, sorting, and pagination so an action always applies to the resources the merchant intended.


Anchor to Replace the toolbar and selection togetherReplace the toolbar and selection together

The example composes select-all and per-row checkboxes, a visible selection count, a promoted action, and a menu of secondary actions. The toolbar replaces the table filters only while resources are selected.

Migrating UnstableBulkActions with table selection

import {useState} from 'react';

const products = [
{id: '1', title: 'Snowboard'},
{id: '2', title: 'Bindings'},
];

interface ProductsBulkActionsProps {
archiveProducts(ids: string[]): Promise<void>;
}

export function ProductsBulkActions({
archiveProducts,
}: ProductsBulkActionsProps) {
const [selectedIds, setSelectedIds] = useState<Set<string>>(
new Set(['1']),
);
const [bulkPending, setBulkPending] = useState(false);
const pageIds = products.map(({id}) => id);
const allSelected = pageIds.every((id) => selectedIds.has(id));
const someSelected =
!allSelected && pageIds.some((id) => selectedIds.has(id));

function togglePage(checked: boolean) {
setSelectedIds(checked ? new Set(pageIds) : new Set());
}

function toggleRow(id: string, checked: boolean) {
setSelectedIds((current) => {
const next = new Set(current);
checked ? next.add(id) : next.delete(id);
return next;
});
}

async function archiveSelection() {
setBulkPending(true);
try {
await archiveProducts([...selectedIds]);
shopify.toast.show(`${selectedIds.size} products archived`);
setSelectedIds(new Set());
} catch {
shopify.toast.show('Products couldn’t be archived', {isError: true});
} finally {
setBulkPending(false);
}
}

return (
<s-section padding="none" accessibilityLabel="Products">
<s-table>
{selectedIds.size > 0 && (
<s-box slot="filters" padding="small" background="strong">
<s-stack
direction="inline"
gap="base"
alignItems="center"
justifyContent="space-between"
>
<s-text type="strong">{selectedIds.size} selected</s-text>
<s-stack direction="inline" gap="small">
<s-button
variant="secondary"
loading={bulkPending}
onClick={archiveSelection}
>
Archive
</s-button>
<s-button commandFor="more-bulk-actions">
More actions
</s-button>
<s-menu
id="more-bulk-actions"
accessibilityLabel="More bulk actions"
>
<s-button variant="tertiary">Add tags</s-button>
<s-button variant="tertiary" tone="critical">
Delete
</s-button>
</s-menu>
</s-stack>
</s-stack>
</s-box>
)}

<s-table-header-row>
<s-table-header listSlot="primary">
<s-stack direction="inline" gap="small" alignItems="center">
<s-checkbox
checked={allSelected}
indeterminate={someSelected}
accessibilityLabel="Select all products on this page"
onChange={(event) =>
togglePage(event.currentTarget.checked)
}
/>
<s-text>Product</s-text>
</s-stack>
</s-table-header>
</s-table-header-row>

<s-table-body>
{products.map((product) => (
<s-table-row key={product.id}>
<s-table-cell>
<s-stack direction="inline" gap="small" alignItems="center">
<s-checkbox
checked={selectedIds.has(product.id)}
accessibilityLabel={`Select ${product.title}`}
onChange={(event) =>
toggleRow(product.id, event.currentTarget.checked)
}
/>
<s-text>{product.title}</s-text>
</s-stack>
</s-table-cell>
</s-table-row>
))}
</s-table-body>
</s-table>
</s-section>
);
}
import {UnstableBulkActions} from '@shopify/polaris';

interface ProductBulkActionsProps {
archiveSelection(): void;
addTags(): void;
deleteSelection(): void;
toggleAll(): void;
}

export function ProductBulkActions({
archiveSelection,
addTags,
deleteSelection,
toggleAll,
}: ProductBulkActionsProps) {
return (
<UnstableBulkActions
selectMode
selected="indeterminate"
label="1 selected"
accessibilityLabel="Select all products on this page"
onToggleAll={toggleAll}
promotedActions={[
{content: 'Archive', onAction: archiveSelection},
]}
actions={[
{content: 'Add tags', onAction: addTags},
{content: 'Delete', onAction: deleteSelection},
]}
/>
);
}

Preview


Anchor to Map selection propertiesMap selection properties

Polaris React propertyPolaris web components composition
selected={true}Set the header s-checkbox checked when every resource on the current page is selected.
selected="indeterminate"Set the header checkbox indeterminate when some, but not all, current-page resources are selected.
onToggleAllHandle the header checkbox change event and add or remove every current-page ID.
labelRender a visible count such as 3 selected in the bulk-action bar. Derive it from app state.
accessibilityLabelPut a specific accessibilityLabel on the select-all checkbox and each row checkbox.
selectModeConditionally render selection checkboxes and the bulk-action bar from app-owned mode or selected-ID state.

Calculate header state from the IDs actually rendered on the page:

  • allOnPageSelected is true when the page isn't empty and every page ID is in selectedIds.
  • someOnPageSelected is true when at least one page ID is selected but allOnPageSelected is false.
  • A row checkbox is checked when its stable resource ID is in selectedIds.

Don't use row indexes as IDs. Sorting, filtering, deletion, and pagination can change an index while an action is pending.


Anchor to Map action descriptorsMap action descriptors

Render the highest-value bulk action as a visible s-button. Put additional actions in s-menu, opened by a button with commandFor.

Polaris React action fieldButton or menu-item migration
contentMove into the button's visible child text.
onActionRename to onClick.
urlRename to href when bulk action navigation is intentional.
disabledKeep disabled on the individual button.
loadingKeep loading on the action whose mutation is pending.
accessibilityLabelKeep only when the visible action label needs additional context.
badgeNo direct equivalent. Make new or important behavior clear in the visible label and surrounding content.

Map promotedActions to visible buttons, but don't reproduce the old measurement logic that moved them into overflow. Choose a small, stable promoted set. Map actions and MenuGroupDescriptor entries to an s-menu; use s-section children inside the menu when groups need headings.

For destructive actions, confirm the operation before sending selected IDs. Keep selection if the mutation fails so the merchant can retry; clear it only after success or an explicit cancel.


Anchor to Migrate selection across pagesMigrate selection across pages

The old paginatedSelectAllText and paginatedSelectAllAction offered a second step after selecting the current page. Preserve the distinction explicitly:

  1. Selecting the header checkbox selects or clears the resources on the current page.

  2. When all current-page resources are selected and more results exist, show text such as All 50 products on this page are selected and a button such as Select all 2,430 matching products.

  3. Store whether selection represents explicit IDs or all resources matching the current query. For query-wide selection, send the filter snapshot plus excluded IDs to the backend instead of loading thousands of IDs into the browser.

  4. Clear query-wide selection when search, filters, sort scope, or the underlying resource set changes enough to make the selection ambiguous.

Show the total scope in destructive confirmations and action results. Delete 2,430 products is safer than Delete selected products.


Removed propertyMigration
buttonSizeRemove it. The destination buttons use their context-appropriate size.
onMoreActionPopoverToggleRemove toolbar state tied only to the old popover. s-menu owns opening, focus, and dismissal.
innerRef or forwarded refMove focus to a meaningful checkbox or action only when the workflow requires it.
onSelectModeToggleRemove the deprecated callback and update app-owned selection mode directly.
isStickyRemove it. Don't create a custom sticky toolbar inside the embedded iframe.
widthRemove the deprecated measurement input. Let the table and action composition respond to available space.

  • Select none, one, some, and every resource on the current page; verify checked and indeterminate states.
  • Change filters, search, sorting, and pages while selection exists, and verify the documented reset or retention behavior.
  • Exercise every promoted and menu action in success, loading, error, and confirmation states.
  • Test query-wide selection, exclusions, and totals against the backend query that performs the mutation.
  • Verify checkbox labels, selection counts, menu keyboard behavior, focus restoration, and destructive confirmations.

Anchor to Remove Polaris ReactRemove Polaris React

After every bulk-action workflow is migrated, remove old action descriptors, measurement helpers, selection wrappers, and the Polaris React UnstableBulkActions import. Remove @shopify/polaris only after no other route in scope imports it.



Was this page helpful?