Skip to main content
Migrate to Polaris

Version 2025-07 is the last API version to support React-based UI components. Later versions use web components, native UI elements with built-in accessibility, better performance, and consistent styling with Shopify's design system. Check out the upgrade guide to upgrade your extension.

Products

Product pages allow merchants to create and manage their product catalog, including product details, variants, inventory, pricing, and media. Extensions on these pages help merchants enrich product data, configure bundles, manage purchase options, or integrate with external systems.

  • Product data enrichment: Enhance product information with supplier data, certifications, sustainability metrics, or extended attributes from external product information management (PIM) systems.
  • Bundle and kit configuration: Configure product bundles, multi-packs, or kits with component selection, pricing rules, and inventory management across bundle components.
  • Subscription and purchase options: Set up subscription plans, pre-order options, or custom purchase terms for products through integrated subscription management platforms.
  • Marketplace publishing: Sync product data to external marketplaces like Amazon, eBay, or Google Shopping, including descriptions, pricing, inventory, and marketplace-specific attributes.
  • Custom label and document generation: Generate product labels, barcodes, spec sheets, or compliance documents based on product attributes and external data sources.
Shopify admin product pages showing all available extension target locations.

Anchor to Product details targetsProduct details targets

Use action and block targets to extend the product details page with workflows and contextual information.

Extensions can query and mutate Shopify data using the direct API, or call your app's backend for custom business logic and external integrations.

admin.product-details.action.render

Renders an admin action extension on the product details page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on product data, such as syncing with external systems, exporting product information, or managing credit terms.

Extensions at this target can access product data through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.

Examples
import React, { useState, useEffect } from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
BlockStack,
Box,
Button,
Checkbox,
ChoiceList,
Divider,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.action.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const { data, close } = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedLocations, setSelectedLocations] = useState<string[]>(['warehouse-main']);
const [overwriteExisting, setOverwriteExisting] = useState(true);

const locationOptions = [
{ label: 'Main Warehouse', value: 'warehouse-main' },
{ label: 'East Coast Fulfillment', value: 'warehouse-east' },
{ label: 'West Coast Fulfillment', value: 'warehouse-west' },
];

const handleSync = async () => {
if (selectedLocations.length === 0) {
setError('Please select at least one location');
return;
}

setLoading(true);
setError(null);
const productId = data.selected[0].id;

try {
const response = await fetch('https://your-app.com/api/inventory/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId,
locations: selectedLocations,
overwriteExisting,
}),
});

if (response.ok) {
const result = await response.json();
setSuccess(true);
close();
} else {
const errorData = await response.json();
setError(errorData.message || 'Failed to sync inventory');
}
} catch (err) {
setError('Connection error. Please try again.');
} finally {
setLoading(false);
}
};

return (
<AdminAction
title="Sync Inventory from Warehouse"
primaryAction={
<Button onPress={handleSync} disabled={loading || success}>
{loading ? 'Syncing...' : 'Sync Inventory'}
</Button>
}
secondaryAction={<Button onPress={close}>Cancel</Button>}
>
<BlockStack gap="base">
{success && (
<Banner tone="success">Inventory synced successfully!</Banner>
)}
{error && (
<Banner tone="critical">{error}</Banner>
)}

<Box padding="base">
<BlockStack gap="base">
<ChoiceList
title="Select warehouse locations"
choices={locationOptions}
value={selectedLocations}
onChange={setSelectedLocations}
/>

<Divider />

<Checkbox
checked={overwriteExisting}
onChange={setOverwriteExisting}
>
Overwrite existing inventory levels
</Checkbox>
</BlockStack>
</Box>
</BlockStack>
</AdminAction>
);
}

Anchor to Product details action (should render) ,[object Object]Product details action (should render) target

admin.product-details.action.should-render

Controls the render state of an admin action extension on the product details page. Use this target to conditionally show or hide your action extension based on the product's properties, such as status, configuration, or specific business requirements.

This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.

Support
Components (0)
APIs (1)

Supported components

-

Available APIs

Examples
import {extension} from '@shopify/ui-extensions/admin';

const TARGET = 'admin.product-details.action.should-render';

export default extension(TARGET, async ({data}) => {
const productId = data.selected[0].id;
const numericId = productId.split('/').pop();

try {
const response = await fetch(
`https://your-app.com/api/catalog/check-product?productId=${numericId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
);

if (!response.ok) {
return {display: false};
}

const result = await response.json();

// Only show action if product is in the custom catalog
// and has catalog-specific features enabled
return {
display: result.isInCatalog && result.catalogFeaturesEnabled,
};
} catch (err) {
console.error('Failed to check catalog status:', err);
return {display: false};
}
});

admin.product-details.block.render

Renders an admin block extension inline on the product details page. Use this target to display contextual information, analytics, or status updates related to the product without requiring merchant interaction to open a modal.

Extensions at this target can access product data through the data property in the Block Extension API. Blocks appear as cards on the page and can show real-time data, insights, or quick actions. They provide persistent visibility for information merchants need to see at a glance.

Examples
import React, { useState, useEffect } from 'react';
import {
reactExtension,
useApi,
AdminBlock,
Banner,
BlockStack,
Box,
Button,
Divider,
Heading,
Icon,
Text,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.block.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const { data } = useApi(TARGET);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [competitors, setCompetitors] = useState([]);
const [lastUpdated, setLastUpdated] = useState(null);

const fetchCompetitorPrices = async () => {
setLoading(true);
setError(null);
const productId = data.selected[0].id;

try {
const response = await fetch('https://your-app.com/api/competitor-prices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId }),
});

if (!response.ok) throw new Error('Failed to fetch prices');

const result = await response.json();
setCompetitors(result.competitors);
setLastUpdated(new Date().toLocaleTimeString());
} catch (err) {
setError('Unable to load competitor pricing data');
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchCompetitorPrices();
}, []);

const getPriceTone = (diff) => {
if (diff > 0) return 'success';
if (diff < 0) return 'critical';
return 'info';
};

return (
<AdminBlock title="Competitor Pricing">
{error && <Banner tone="critical">{error}</Banner>}
{loading ? (
<Text>Loading competitor prices...</Text>
) : (
<BlockStack gap="base">
{competitors.map((competitor, index) => (
<Box key={index} padding="base" background="subdued" borderRadius="base">
<BlockStack gap="tight">
<InlineStack align="space-between">
<Text fontWeight="bold">{competitor.name}</Text>
<Text fontWeight="bold">${competitor.price.toFixed(2)}</Text>
</InlineStack>
<InlineStack gap="tight" align="start">
<Icon name={competitor.priceDiff > 0 ? 'ArrowDown' : 'ArrowUp'} />
<Text tone={getPriceTone(competitor.priceDiff)}>
{competitor.priceDiff > 0 ? 'Lower' : 'Higher'} by ${Math.abs(competitor.priceDiff).toFixed(2)}
</Text>
</InlineStack>
</BlockStack>
</Box>
))}
<Divider />
<BlockStack gap="tight">
<Text appearance="subdued">Last updated: {lastUpdated}</Text>
<Button onPress={fetchCompetitorPrices}>Refresh Prices</Button>
</BlockStack>
</BlockStack>
)}
</AdminBlock>
);
}

function InlineStack({ children, align, gap }) {
return <BlockStack gap={gap}>{children}</BlockStack>;
}

admin.product-details.configuration.render

Renders a configuration interface for product bundles on product details pages. This target allows merchants to configure component products, quantities, and pricing for bundle configurations at the product level. Use this target when your app needs to provide merchant-facing configuration UI for bundle components and options.

Learn how to add a product configuration extension.

Examples
import React, { useState, useEffect } from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
BlockStack,
Box,
Button,
Checkbox,
ChoiceList,
Divider,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.configuration.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const { data, close } = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedLocations, setSelectedLocations] = useState<string[]>(['warehouse-main']);
const [overwriteExisting, setOverwriteExisting] = useState(true);

const locationOptions = [
{ label: 'Main Warehouse', value: 'warehouse-main' },
{ label: 'East Coast Fulfillment', value: 'warehouse-east' },
{ label: 'West Coast Fulfillment', value: 'warehouse-west' },
];

const handleSync = async () => {
if (selectedLocations.length === 0) {
setError('Please select at least one location');
return;
}

setLoading(true);
setError(null);
const productId = data.selected[0].id;

try {
const response = await fetch('https://your-app.com/api/inventory/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId,
locations: selectedLocations,
overwriteExisting,
}),
});

if (response.ok) {
const result = await response.json();
setSuccess(true);
close();
} else {
const errorData = await response.json();
setError(errorData.message || 'Failed to sync inventory');
}
} catch (err) {
setError('Connection error. Please try again.');
} finally {
setLoading(false);
}
};

return (
<AdminAction
title="Sync Inventory from Warehouse"
primaryAction={
<Button onPress={handleSync} disabled={loading || success}>
{loading ? 'Syncing...' : 'Sync Inventory'}
</Button>
}
secondaryAction={<Button onPress={close}>Cancel</Button>}
>
<BlockStack gap="base">
{success && (
<Banner tone="success">Inventory synced successfully!</Banner>
)}
{error && (
<Banner tone="critical">{error}</Banner>
)}

<Box padding="base">
<BlockStack gap="base">
<ChoiceList
title="Select warehouse locations"
choices={locationOptions}
value={selectedLocations}
onChange={setSelectedLocations}
/>

<Divider />

<Checkbox
checked={overwriteExisting}
onChange={setOverwriteExisting}
>
Overwrite existing inventory levels
</Checkbox>
</BlockStack>
</Box>
</BlockStack>
</AdminAction>
);
}

admin.product-details.print-action.render

Renders a print action extension on the product details page that merchants can access from the Print menu. Use this target to generate custom printable documents like product labels, barcodes, specification sheets, or compliance documents. Extensions at this target can access the product ID through the data property in the Action Extension API and use the direct API to fetch complete product details before generating print output.

Examples
import React, { useState } from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
BlockStack,
Box,
Button,
Checkbox,
ChoiceList,
Divider,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.print-action.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const { data, close } = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [labelSize, setLabelSize] = useState(['standard']);
const [includeBarcode, setIncludeBarcode] = useState(true);
const [includePrice, setIncludePrice] = useState(true);

const handleGenerateLabels = async () => {
setLoading(true);
setError(null);
const productId = data.selected[0].id;

try {
const response = await fetch('https://your-app.com/api/generate-labels', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId,
labelSize: labelSize[0],
includeBarcode,
includePrice,
}),
});

if (response.ok) {
const result = await response.json();
setSuccess(true);
close();
} else {
const errorData = await response.json();
setError(errorData.message || 'Failed to generate labels');
}
} catch (err) {
setError('Network error. Please try again.');
} finally {
setLoading(false);
}
};

return (
<AdminAction
title="Generate Product Labels"
primaryAction={
<Button onPress={handleGenerateLabels} disabled={loading || success}>
{loading ? 'Generating...' : 'Generate & Print'}
</Button>
}
secondaryAction={<Button onPress={close}>Cancel</Button>}
>
<BlockStack gap="base">
{success && (
<Banner tone="success">Labels generated! Opening print dialog...</Banner>
)}
{error && <Banner tone="critical">{error}</Banner>}

<Box padding="base">
<BlockStack gap="base">
<ChoiceList
name="labelSize"
title="Label Size"
value={labelSize}
onChange={setLabelSize}
choices={[
{ id: 'small', label: 'Small (1" x 0.5")' },
{ id: 'standard', label: 'Standard (2" x 1")' },
{ id: 'large', label: 'Large (4" x 2")' },
]}
/>

<Divider />

<Checkbox
checked={includeBarcode}
onChange={setIncludeBarcode}
>
Include barcode
</Checkbox>

<Checkbox
checked={includePrice}
onChange={setIncludePrice}
>
Include price
</Checkbox>
</BlockStack>
</Box>
</BlockStack>
</AdminAction>
);
}

Anchor to Product details print action (should render) ,[object Object]Product details print action (should render) target

admin.product-details.print-action.should-render

Controls the render state of an admin action extension on the product details page. Use this target to conditionally show or hide your action extension based on the product's properties, such as status, configuration, or specific business requirements.

This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.

Support
Components (0)
APIs (1)

Supported components

-

Available APIs

Examples
import React from 'react';
import {
reactExtension,
useApi,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.print-action.should-render';

export default reactExtension(TARGET, async (api) => {
const productId = api.data.selected[0].id;
const token = await api.session?.getSessionToken();
try {
// Check with app backend if product has printable labels
const response = await fetch('https://your-app.com/api/check-print-eligibility', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ productId }),
});

if (!response.ok) {
return { display: false };
}

const result = await response.json();
// Backend returns whether product has labels configured
// and whether user has print permissions
return {
display: result.hasLabels && result.userCanPrint,
};
} catch (err) {
// Hide action if backend check fails
console.error('Print eligibility check failed:', err);
return { display: false };
}
});

admin.product-details.reorder.render

Renders a block extension that provides custom reordering functionality on the product details page. This target allows you to display reorder controls, quick reorder buttons, or inventory replenishment workflows directly within the product editor. Use this target when your app needs to help merchants quickly restock or reorder products based on inventory levels or sales velocity.

Examples
import React from 'react';
import { useState, useEffect } from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
BlockStack,
Box,
Button,
Checkbox,
ChoiceList,
Divider,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.reorder.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const { data, close } = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState('');
const [selectedLocations, setSelectedLocations] = useState(['warehouse-main']);
const [overwriteExisting, setOverwriteExisting] = useState(true);

const locations = [
{ label: 'Main Warehouse', value: 'warehouse-main' },
{ label: 'East Coast Fulfillment', value: 'warehouse-east' },
{ label: 'West Coast Fulfillment', value: 'warehouse-west' },
];

const handleSync = async () => {
if (selectedLocations.length === 0) {
setError('Please select at least one location');
return;
}

setLoading(true);
setError('');
const productId = data.selected[0].id;

try {
const response = await fetch('https://your-app.com/api/inventory/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId,
locations: selectedLocations,
overwriteExisting,
}),
});

if (response.ok) {
const result = await response.json();
setSuccess(true);
close();
} else {
const errorData = await response.json();
setError(errorData.message || 'Failed to sync inventory');
}
} catch (err) {
setError('Connection error. Please try again.');
} finally {
setLoading(false);
}
};

return (
<AdminAction
title="Sync Inventory from Warehouse"
primaryAction={
<Button onPress={handleSync} disabled={loading || success}>
{loading ? 'Syncing...' : 'Sync Inventory'}
</Button>
}
secondaryAction={<Button onPress={close}>Cancel</Button>}
>
<BlockStack gap="base">
{success && (
<Banner tone="success">Inventory synced successfully!</Banner>
)}
{error && <Banner tone="critical">{error}</Banner>}

<Box>
<ChoiceList
title="Select warehouse locations"
choices={locations}
value={selectedLocations}
onChange={setSelectedLocations}
/>
</Box>

<Divider />

<Checkbox
checked={overwriteExisting}
onChange={setOverwriteExisting}
>
Overwrite existing inventory levels
</Checkbox>
</BlockStack>
</AdminAction>
);
}

Anchor to Product index targetsProduct index targets

Use action targets to extend the product index page with bulk operations and workflows that help merchants manage multiple products efficiently.

admin.product-index.action.render

Renders an admin action extension on the product index page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on product data, such as syncing with external systems, exporting product information, or managing credit terms.

Extensions at this target can access product data through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.

Examples
import React from 'react';
import { useState } from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
BlockStack,
Box,
Button,
Checkbox,
ChoiceList,
Divider,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-index.action.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const { data, close } = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [warehouse, setWarehouse] = useState(['main']);
const [overwriteZero, setOverwriteZero] = useState(false);

const selectedCount = data.selected.length;

const handleSync = async () => {
setLoading(true);
setError(null);

const productIds = data.selected.map((item) => item.id);

try {
const response = await fetch('https://your-app.com/api/inventory/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productIds,
warehouse: warehouse[0],
overwriteZero,
}),
});

if (response.ok) {
const result = await response.json();
setSuccess(true);
close();
} else {
const errorData = await response.json();
setError(errorData.message || 'Failed to sync inventory');
}
} catch (err) {
setError('Connection error. Please try again.');
} finally {
setLoading(false);
}
};

return (
<AdminAction
title="Sync Inventory from Warehouse"
primaryAction={
<Button onPress={handleSync} disabled={loading || success || selectedCount === 0}>
{loading ? 'Syncing...' : `Sync ${selectedCount} Product${selectedCount !== 1 ? 's' : ''}`}
</Button>
}
secondaryAction={<Button onPress={close}>Cancel</Button>}
>
<BlockStack gap="base">
{success && (
<Banner tone="success">
Inventory synced successfully for {selectedCount} product(s)!
</Banner>
)}
{error && <Banner tone="critical">{error}</Banner>}

<Box padding="base">
<BlockStack gap="base">
<ChoiceList
name="warehouse"
title="Select Warehouse"
value={warehouse}
onChange={setWarehouse}
choices={[
{ label: 'Main Warehouse (US)', id: 'main' },
{ label: 'East Coast Fulfillment', id: 'east' },
{ label: 'West Coast Fulfillment', id: 'west' },
]}
/>

<Divider />

<Checkbox
checked={overwriteZero}
onChange={setOverwriteZero}
>
Update products even if warehouse shows zero stock
</Checkbox>
</BlockStack>
</Box>
</BlockStack>
</AdminAction>
);
}

Anchor to Product index action (should render) ,[object Object]Product index action (should render) target

admin.product-index.action.should-render

Controls the render state of an admin action extension on the product index page. Use this target to conditionally show or hide your action extension based on the product's properties, such as status, configuration, or specific business requirements.

This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.

Support
Components (0)
APIs (1)

Supported components

-

Available APIs

Examples
import {extension} from '@shopify/ui-extensions/admin';

// Should-render targets use extension() - no React variant
export default extension(
'admin.product-index.action.should-render',
async ({data}) => {
// Get all selected product IDs
const selectedIds = data.selected.map((item) => item.id);

if (selectedIds.length === 0) {
return {display: false};
}

try {
// Call your app backend to check if products are in approved categories
const response = await fetch('https://your-app.com/api/products/check-eligibility', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productIds: selectedIds,
}),
});

if (!response.ok) {
return {display: false};
}

const result = await response.json();

// Backend returns whether all selected products are eligible
// based on category rules, inventory status, or other business logic
return {display: result.allEligible === true};
} catch (err) {
// Hide action if backend check fails
return {display: false};
}
}
);

admin.product-index.selection-action.render

Renders a selection action extension on the product index page when merchants select multiple products. Merchants can access this extension from the More actions menu. Use this target to provide bulk operations that work on multiple products simultaneously, such as batch tagging, bulk export, price updates, or marketplace publishing. Extensions at this target can access all selected product IDs through the data property in the Action Extension API.

Examples
import React, { useState } from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
BlockStack,
Box,
Button,
Checkbox,
ChoiceList,
Divider,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-index.selection-action.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const { data, close } = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [warehouse, setWarehouse] = useState(['main']);
const [overwriteExisting, setOverwriteExisting] = useState(true);

const selectedCount = data.selected.length;

const handleSync = async () => {
setLoading(true);
setError(null);

const productIds = data.selected.map((item) => item.id);

try {
const response = await fetch('https://your-app.com/api/inventory/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productIds,
warehouse: warehouse[0],
overwriteExisting,
}),
});

if (response.ok) {
const result = await response.json();
setSuccess(true);
close();
} else {
const errorData = await response.json();
setError(errorData.message || 'Failed to sync inventory');
}
} catch (err) {
setError('Connection error. Please try again.');
} finally {
setLoading(false);
}
};

return (
<AdminAction
title="Sync Inventory from Warehouse"
primaryAction={
<Button onPress={handleSync} disabled={loading || success}>
{loading ? 'Syncing...' : `Sync ${selectedCount} Products`}
</Button>
}
secondaryAction={<Button onPress={close}>Cancel</Button>}
>
<BlockStack gap="base">
{success && (
<Banner tone="success">
Inventory synced successfully for {selectedCount} products!
</Banner>
)}
{error && <Banner tone="critical">{error}</Banner>}

<Box padding="base">
<BlockStack gap="base">
<ChoiceList
name="warehouse"
title="Select Warehouse"
value={warehouse}
onChange={setWarehouse}
choices={[
{ label: 'Main Warehouse', id: 'main' },
{ label: 'East Coast Fulfillment', id: 'east' },
{ label: 'West Coast Fulfillment', id: 'west' },
]}
/>

<Divider />

<Checkbox
checked={overwriteExisting}
onChange={setOverwriteExisting}
>
Overwrite existing inventory levels
</Checkbox>
</BlockStack>
</Box>
</BlockStack>
</AdminAction>
);
}

Anchor to Product index selection action (should render) ,[object Object]Product index selection action (should render) target

admin.product-index.selection-action.should-render

Controls the render state of a selection action extension on the product index page. Use this target to conditionally show or hide your action extension based on the product's properties, such as status, configuration, or specific business requirements.

This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.

Support
Components (0)
APIs (1)

Supported components

-

Available APIs

Examples
import {extension} from '@shopify/ui-extensions/admin';

export default extension(
'admin.product-index.selection-action.should-render',
async ({data}) => {
const selectedProductIds = data.selected.map((item) => item.id);

try {
// Call your app backend to check if products are in your custom catalog
const response = await fetch('https://your-app.com/api/catalog/check-eligibility', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productIds: selectedProductIds,
}),
});

if (!response.ok) {
return {display: false};
}

const result = await response.json();

// Show action only if at least one product is eligible for catalog actions
return {display: result.hasEligibleProducts === true};
} catch (err) {
// Hide action if backend check fails
return {display: false};
}
}
);

Anchor to Product index selection print action ,[object Object]Product index selection print action target

admin.product-index.selection-print-action.render

Renders a print action extension on the product index page when merchants select multiple products. Merchants can access this extension from the Print menu. Use this target to generate batch print documents like barcode labels, price tags, inventory sheets, or product catalogs for multiple products at once. Extensions at this target can access all selected product IDs through the data property in the Action Extension API and use the direct API to fetch complete product details for print generation.

Examples
import React, { useState, useEffect } from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
BlockStack,
Box,
Button,
Checkbox,
ChoiceList,
Divider,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-index.selection-print-action.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const { data, close } = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [warehouse, setWarehouse] = useState(['main']);
const [overwriteExisting, setOverwriteExisting] = useState(true);
const [syncCount, setSyncCount] = useState(0);

const selectedProducts = data.selected.map(item => item.id);

const handleSync = async () => {
setLoading(true);
setError(null);

try {
const response = await fetch('https://your-app.com/api/inventory/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productIds: selectedProducts,
warehouse: warehouse[0],
overwriteExisting,
}),
});

if (!response.ok) {
throw new Error('Failed to sync inventory');
}

const result = await response.json();
setSyncCount(result.updatedCount);
setSuccess(true);
close();
} catch (err) {
setError(err instanceof Error ? err.message : 'Sync failed');
} finally {
setLoading(false);
}
};

return (
<AdminAction
title="Sync Inventory from Warehouse"
primaryAction={
<Button onPress={handleSync} disabled={loading || success}>
{loading ? 'Syncing...' : `Sync ${selectedProducts.length} Products`}
</Button>
}
secondaryAction={<Button onPress={close}>Cancel</Button>}
>
<BlockStack gap="base">
{success && (
<Banner tone="success">
Successfully synced {syncCount} inventory levels!
</Banner>
)}
{error && <Banner tone="critical">{error}</Banner>}

<Box padding="base">
<BlockStack gap="base">
<ChoiceList
name="warehouse"
title="Select Warehouse"
value={warehouse}
onChange={setWarehouse}
choices={[
{ label: 'Main Warehouse', id: 'main' },
{ label: 'East Coast Fulfillment', id: 'east' },
{ label: 'West Coast Fulfillment', id: 'west' },
]}
/>

<Divider />

<Checkbox
checked={overwriteExisting}
onChange={setOverwriteExisting}
>
Overwrite existing inventory levels
</Checkbox>
</BlockStack>
</Box>

<Banner tone="info">
{selectedProducts.length} product(s) selected for inventory sync
</Banner>
</BlockStack>
</AdminAction>
);
}

Anchor to Product index selection print action (should render) ,[object Object]Product index selection print action (should render) target

admin.product-index.selection-print-action.should-render

Controls the render state of an admin action extension on the product index page. Use this target to conditionally show or hide your action extension based on the product's properties, such as status, configuration, or specific business requirements.

This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.

Support
Components (0)
APIs (1)

Supported components

-

Available APIs

Examples
import React from 'react';
import { reactExtension, useApi } from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-index.selection-print-action.should-render';

export default reactExtension(TARGET, async (api) => {
const { data, auth } = api;
const selectedIds = data.selected.map((item) => item.id);

try {
const token = await auth.getSessionToken();
const response = await fetch('https://your-app.com/api/inventory/check-sync-eligibility', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ productIds: selectedIds }),
});

if (!response.ok) {
return { display: false };
}

const result = await response.json();
// Show action only if products are registered in warehouse system
// and have pending inventory updates
return {
display: result.eligibleForSync && result.hasPendingUpdates,
};
} catch (err) {
// Hide action if we can't verify eligibility
return { display: false };
}
});

Anchor to Product purchase option targetsProduct purchase option targets

Use action targets to extend the product purchase option page with workflows and operations.

Extensions can query and mutate Shopify data using the direct API, or call your app's backend for custom business logic and external integrations.

admin.product-purchase-option.action.render

Renders an admin action extension on the product details page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on product data, such as syncing with external systems, exporting product information, or managing credit terms.

Extensions at this target can access product data through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.

Examples
import React from 'react';
import { useState } from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
BlockStack,
Box,
Button,
Checkbox,
ChoiceList,
Divider,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-purchase-option.action.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const { data, close } = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [syncOptions, setSyncOptions] = useState(['inventory']);
const [overwriteExisting, setOverwriteExisting] = useState(false);

const handleSync = async () => {
setLoading(true);
setError(null);
const purchaseOptionId = data.selected[0].id;

try {
const response = await fetch('https://your-app.com/api/sync-purchase-option', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
purchaseOptionId,
syncOptions,
overwriteExisting,
}),
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Sync failed');
}

setSuccess(true);
close();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to sync with inventory system');
} finally {
setLoading(false);
}
};

return (
<AdminAction
title="Sync with Inventory System"
primaryAction={
<Button onPress={handleSync} disabled={loading || success || syncOptions.length === 0}>
{loading ? 'Syncing...' : 'Sync Now'}
</Button>
}
secondaryAction={<Button onPress={close}>Cancel</Button>}
>
<BlockStack gap="base">
{success && (
<Banner tone="success">Purchase option synced successfully!</Banner>
)}
{error && (
<Banner tone="critical">{error}</Banner>
)}

<Box padding="base">
<BlockStack gap="base">
<ChoiceList
name="syncOptions"
title="Select data to sync"
value={syncOptions}
onChange={setSyncOptions}
choices={[
{ label: 'Inventory allocation rules', value: 'inventory' },
{ label: 'Fulfillment settings', value: 'fulfillment' },
{ label: 'Pricing tiers', value: 'pricing' },
]}
/>

<Divider />

<Checkbox
checked={overwriteExisting}
onChange={setOverwriteExisting}
>
Overwrite existing settings in inventory system
</Checkbox>
</BlockStack>
</Box>
</BlockStack>
</AdminAction>
);
}

  • Query only the fields you need: When fetching product data using GraphQL, request only the specific fields your extension displays. For example, if you're showing inventory levels, don't fetch media or metafields. This reduces query costs and improves extension load times.
  • Handle product status in workflows: Before syncing products to external systems or marketplaces, check the product's status (active, draft, archived). Some workflows should only operate on active products, while others may need to handle all statuses differently.
  • Validate bundle components before saving: When building bundle configuration extensions, verify that all component products are active and have available inventory before allowing merchants to save the bundle configuration. This prevents merchants from creating bundles with unavailable components.
  • Consider variant count in displays: Products with many variants require different UI approaches than simple products. When displaying variant data, implement pagination or collapsible sections for products with many variants to maintain good performance.
  • Handle product-level vs variant-level data correctly: Some data lives at the product level (title, description, media) while other data is variant-specific (SKU, price, inventory). When syncing to external systems, ensure you're pulling data from the correct level to avoid inconsistencies.

  • Single target per module: Each [[extensions.targeting]] entry in your TOML configuration maps one target to one module file.
  • Purchase option target visibility: The admin.product-purchase-option.action.render target only appears when the product has a selling plan group associated with it.
  • Configuration target availability: The admin.product-details.configuration.render target only appears for products configured as bundles.
  • Print menu location: Print actions appear in the Print menu, not the More actions menu.
  • Block target visibility: Block extensions must be manually added and pinned by merchants before they appear.
  • Block collapse behavior: Returning null from a block extension collapses the block rather than removing it from the page. Blocks can't be fully hidden at runtime.

Was this page helpful?