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.
Anchor to Use casesUse cases
- 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.

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.
Anchor to Product details action ,[object Object]Product details action target
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.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add an action extension that syncs product inventory levels from an external warehouse management system. This example demonstrates calling your app backend to fetch real-time stock data and update inventory across multiple locations.
React
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> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let selectedLocations: string[] = ['warehouse-main']; let overwriteExisting = 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) { error = 'Please select at least one location'; updateUI(); return; } loading = true; error = null; updateUI(); const productId = api.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) { success = true; updateUI(); api.close(); } else { const errorData = await response.json(); error = errorData.message || 'Failed to sync inventory'; updateUI(); } } catch (err) { error = 'Connection error. Please try again.'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent( Button, { onPress: handleSync, disabled: loading || success }, loading ? 'Syncing...' : 'Sync Inventory' ) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Inventory synced successfully!') ); } if (error) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { title: 'Select warehouse locations', choices: locationOptions, value: selectedLocations, onChange: (value: string[]) => { selectedLocations = value; updateUI(); }, }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent( Checkbox, { checked: overwriteExisting, onChange: (value: boolean) => { overwriteExisting = value; updateUI(); }, }, 'Overwrite existing inventory levels' ) ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Sync Inventory from Warehouse', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that publishes a product to an external marketplace by fetching product details using the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access) query() method to retrieve title, description, variants, and pricing before syncing to the marketplace.
React
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, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [publishing, setPublishing] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(''); const [product, setProduct] = useState(null); const [selectedMarketplaces, setSelectedMarketplaces] = useState(['amazon']); const [syncInventory, setSyncInventory] = useState(true); useEffect(() => { fetchProductDetails(); }, []); const fetchProductDetails = async () => { const productId = data.selected[0].id; try { const result = await query( `query GetProduct($id: ID!) { product(id: $id) { id title description status totalInventory variants(first: 10) { edges { node { id title price sku inventoryQuantity } } } } }`, { variables: { id: productId } } ); if (result.data?.product) { setProduct(result.data.product); } else { setError('Failed to load product details'); } } catch (err) { setError('Error fetching product data'); } finally { setLoading(false); } }; const handlePublish = async () => { setPublishing(true); setError(''); try { const response = await fetch('https://your-app.com/api/publish-to-marketplace', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product, marketplaces: selectedMarketplaces, syncInventory, }), }); if (response.ok) { setSuccess(true); close(); } else { setError('Failed to publish product'); } } catch (err) { setError('Connection error. Please try again.'); } finally { setPublishing(false); } }; if (loading) { return ( <AdminAction title="Publish to Marketplace"> <Banner tone="info">Loading product details...</Banner> </AdminAction> ); } return ( <AdminAction title="Publish to Marketplace" primaryAction={ <Button onPress={handlePublish} disabled={publishing || success || !product}> {publishing ? 'Publishing...' : 'Publish'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && <Banner tone="success">Product published successfully!</Banner>} {error && <Banner tone="critical">{error}</Banner>} {product && ( <> <Box padding="base"> <BlockStack gap="tight"> <Banner tone="info"> {product.title} • {product.variants.edges.length} variant(s) • {product.totalInventory} in stock </Banner> </BlockStack> </Box> <Divider /> <Box padding="base"> <ChoiceList title="Select Marketplaces" choices={[ { id: 'amazon', label: 'Amazon' }, { id: 'ebay', label: 'eBay' }, { id: 'walmart', label: 'Walmart Marketplace' }, ]} value={selectedMarketplaces} onChange={setSelectedMarketplaces} /> </Box> <Divider /> <Box padding="base"> <Checkbox checked={syncInventory} onChange={setSyncInventory} > Keep inventory synced across marketplaces </Checkbox> </Box> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.action.render', async (root, api) => { let loading = true; let publishing = false; let success = false; let error = ''; let product: any = null; let selectedMarketplaces = ['amazon']; let syncInventory = true; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent( Button, { onPress: handlePublish, disabled: publishing || success || !product }, publishing ? 'Publishing...' : 'Publish' ) ); if (loading) { content.appendChild( root.createComponent(Banner, { tone: 'info' }, 'Loading product details...') ); return; } const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Product published successfully!') ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } if (product) { const infoBox = root.createComponent(Box, { padding: 'base' }); const infoStack = root.createComponent(BlockStack, { gap: 'tight' }); infoStack.appendChild( root.createComponent( Banner, { tone: 'info' }, `${product.title} • ${product.variants.edges.length} variant(s) • ${product.totalInventory} in stock` ) ); infoBox.appendChild(infoStack); stack.appendChild(infoBox); stack.appendChild(root.createComponent(Divider, {})); const marketplaceBox = root.createComponent(Box, { padding: 'base' }); marketplaceBox.appendChild( root.createComponent(ChoiceList, { title: 'Select Marketplaces', choices: [ { id: 'amazon', label: 'Amazon' }, { id: 'ebay', label: 'eBay' }, { id: 'walmart', label: 'Walmart Marketplace' }, ], value: selectedMarketplaces, onChange: (value: string[]) => { selectedMarketplaces = value; updateUI(); }, }) ); stack.appendChild(marketplaceBox); stack.appendChild(root.createComponent(Divider, {})); const syncBox = root.createComponent(Box, { padding: 'base' }); syncBox.appendChild( root.createComponent( Checkbox, { checked: syncInventory, onChange: (value: boolean) => { syncInventory = value; updateUI(); }, }, 'Keep inventory synced across marketplaces' ) ); stack.appendChild(syncBox); } content.appendChild(stack); }; const handlePublish = async () => { publishing = true; error = ''; updateUI(); try { const response = await fetch('https://your-app.com/api/publish-to-marketplace', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product, marketplaces: selectedMarketplaces, syncInventory }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { error = 'Failed to publish product'; } } catch (err) { error = 'Connection error. Please try again.'; } finally { publishing = false; updateUI(); } }; const fetchProductDetails = async () => { const productId = api.data.selected[0].id; try { const result = await api.query( `query GetProduct($id: ID!) { product(id: $id) { id title description status totalInventory variants(first: 10) { edges { node { id title price sku inventoryQuantity } } } } }`, { variables: { id: productId } } ); if (result.data?.product) { product = result.data.product; } else { error = 'Failed to load product details'; } } catch (err) { error = 'Error fetching product data'; } finally { loading = false; updateUI(); } }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Publish to Marketplace', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); await fetchProductDetails(); } );
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.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Add an action extension that checks your app backend to determine if a product is part of a custom catalog before displaying the action. This example demonstrates calling an external API endpoint to verify product eligibility based on custom business logic stored in your app's database.
React
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}; } });TS
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}; } });Description
Add an action extension that only displays for products that have stock available. This example uses the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access) to query the [GraphQL Admin API](/docs/api/admin-graphql) and check if the product has any variants with inventory in stock.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.action.should-render', async ({data, query}) => { const productId = data.selected[0].id; const INVENTORY_QUERY = ` query GetProductInventory($id: ID!) { product(id: $id) { totalInventory tracksInventory variants(first: 10) { nodes { inventoryQuantity inventoryPolicy } } } } `; try { const response = await query(INVENTORY_QUERY, { variables: { id: productId } }); const product = response.data?.product; if (!product) { return { display: false }; } // Show action if product doesn't track inventory (always available) if (!product.tracksInventory) { return { display: true }; } // Show action if total inventory is greater than zero if (product.totalInventory > 0) { return { display: true }; } // Also show if any variant allows overselling (CONTINUE policy) const hasOversellVariant = product.variants.nodes.some( (variant) => variant.inventoryPolicy === 'CONTINUE' ); return { display: hasOversellVariant }; } catch (err) { console.error('Failed to check inventory status:', err); return { display: false }; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.action.should-render', async ({data, query}) => { const productId = data.selected[0].id; const INVENTORY_QUERY = ` query GetProductInventory($id: ID!) { product(id: $id) { totalInventory tracksInventory variants(first: 10) { nodes { inventoryQuantity inventoryPolicy } } } } `; try { const response = await query(INVENTORY_QUERY, { variables: { id: productId } }); const product = response.data?.product; if (!product) { return { display: false }; } // Show action if product doesn't track inventory (always available) if (!product.tracksInventory) { return { display: true }; } // Show action if total inventory is greater than zero if (product.totalInventory > 0) { return { display: true }; } // Also show if any variant allows overselling (CONTINUE policy) const hasOversellVariant = product.variants.nodes.some( (variant: { inventoryPolicy: string }) => variant.inventoryPolicy === 'CONTINUE' ); return { display: hasOversellVariant }; } catch (err) { console.error('Failed to check inventory status:', err); return { display: false }; } } );
Anchor to Product details block ,[object Object]Product details block target
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.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Create a block extension that shows competitor prices for the current product by fetching data from your app backend. This example demonstrates calling your app's API endpoint to retrieve and display competitive pricing intelligence.
React
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>; }TS
import { extension, AdminBlock, Banner, BlockStack, Box, Button, Divider, Heading, Icon, Text, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.block.render', (root, api) => { let loading = true; let error: string | null = null; let competitors: Array<{name: string; price: number; priceDiff: number}> = []; let lastUpdated: string | null = null; const content = root.createFragment(); const fetchCompetitorPrices = async () => { loading = true; error = null; updateUI(); const productId = api.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(); competitors = result.competitors; lastUpdated = new Date().toLocaleTimeString(); } catch (err) { error = 'Unable to load competitor pricing data'; } finally { loading = false; updateUI(); } }; const updateUI = () => { content.replaceChildren(); if (error) { content.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } if (loading) { content.appendChild( root.createComponent(Text, {}, 'Loading competitor prices...') ); } else { const stack = root.createComponent(BlockStack, { gap: 'base' }); competitors.forEach((competitor) => { const box = root.createComponent(Box, { padding: 'base', background: 'subdued', borderRadius: 'base', }); const innerStack = root.createComponent(BlockStack, { gap: 'tight' }); innerStack.appendChild( root.createComponent(Text, { fontWeight: 'bold' }, competitor.name) ); innerStack.appendChild( root.createComponent(Text, { fontWeight: 'bold' }, `$${competitor.price.toFixed(2)}`) ); const diffText = competitor.priceDiff > 0 ? 'Lower' : 'Higher'; const diffTone = competitor.priceDiff > 0 ? 'success' : 'critical'; innerStack.appendChild( root.createComponent( Text, { tone: diffTone }, `${diffText} by $${Math.abs(competitor.priceDiff).toFixed(2)}` ) ); box.appendChild(innerStack); stack.appendChild(box); }); stack.appendChild(root.createComponent(Divider, {})); const footerStack = root.createComponent(BlockStack, { gap: 'tight' }); footerStack.appendChild( root.createComponent(Text, { appearance: 'subdued' }, `Last updated: ${lastUpdated}`) ); footerStack.appendChild( root.createComponent(Button, { onPress: fetchCompetitorPrices }, 'Refresh Prices') ); stack.appendChild(footerStack); content.appendChild(stack); } }; const adminBlock = root.createComponent(AdminBlock, { title: 'Competitor Pricing', }); adminBlock.appendChild(content); root.appendChild(adminBlock); fetchCompetitorPrices(); root.mount(); } );Description
Create a block extension that shows low stock warnings and reorder points for product variants using the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access) to fetch inventory levels from the [GraphQL Admin API](/docs/api/admin-graphql).
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminBlock, Banner, BlockStack, Box, Divider, Heading, Icon, Button, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.product-details.block.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [variants, setVariants] = useState([]); const [error, setError] = useState(null); const REORDER_THRESHOLD = 10; const fetchInventory = async () => { setLoading(true); setError(null); const productId = data.selected[0].id; try { const result = await query( `query GetProductInventory($id: ID!) { product(id: $id) { title variants(first: 20) { nodes { id title inventoryQuantity inventoryItem { tracked } } } } }`, { variables: { id: productId } } ); if (result.data?.product?.variants?.nodes) { setVariants(result.data.product.variants.nodes); } } catch (err) { setError('Failed to load inventory data'); } finally { setLoading(false); } }; useEffect(() => { fetchInventory(); }, []); const lowStockVariants = variants.filter( v => v.inventoryItem?.tracked && v.inventoryQuantity <= REORDER_THRESHOLD ); const outOfStockVariants = variants.filter( v => v.inventoryItem?.tracked && v.inventoryQuantity <= 0 ); if (loading) { return ( <AdminBlock title="Inventory Alerts"> <BlockStack> <Banner tone="info">Loading inventory data...</Banner> </BlockStack> </AdminBlock> ); } return ( <AdminBlock title="Inventory Alerts"> <BlockStack gap="base"> {error && <Banner tone="critical">{error}</Banner>} {outOfStockVariants.length > 0 && ( <Banner tone="critical"> <Icon name="AlertCircle" /> {outOfStockVariants.length} variant(s) out of stock </Banner> )} {lowStockVariants.length > 0 && outOfStockVariants.length === 0 && ( <Banner tone="warning"> <Icon name="AlertTriangle" /> {lowStockVariants.length} variant(s) below reorder point </Banner> )} {lowStockVariants.length === 0 && outOfStockVariants.length === 0 && ( <Banner tone="success">All variants have healthy stock levels</Banner> )} <Divider /> <Heading size="small">Low Stock Details (≤{REORDER_THRESHOLD} units)</Heading> {lowStockVariants.map((variant) => ( <Box key={variant.id} padding="small" background="subdued" borderRadius="base"> <BlockStack gap="extraTight"> <Heading size="small">{variant.title}</Heading> <Box> Stock: {variant.inventoryQuantity} | Reorder at: {REORDER_THRESHOLD} </Box> </BlockStack> </Box> ))} {lowStockVariants.length === 0 && ( <Box padding="small">No variants need restocking</Box> )} <Button onPress={fetchInventory}>Refresh Inventory</Button> </BlockStack> </AdminBlock> ); }TS
import { extension, AdminBlock, Banner, BlockStack, Box, Divider, Heading, Icon, Button, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.block.render', (root, api) => { const REORDER_THRESHOLD = 10; let variants = []; let loading = true; let error = null; const content = root.createFragment(); const fetchInventory = async () => { loading = true; error = null; updateUI(); const productId = api.data.selected[0].id; try { const result = await api.query( `query GetProductInventory($id: ID!) { product(id: $id) { title variants(first: 20) { nodes { id title inventoryQuantity inventoryItem { tracked } } } } }`, { variables: { id: productId } } ); if (result.data?.product?.variants?.nodes) { variants = result.data.product.variants.nodes; } } catch (err) { error = 'Failed to load inventory data'; } finally { loading = false; updateUI(); } }; const updateUI = () => { content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (loading) { stack.appendChild( root.createComponent(Banner, { tone: 'info' }, 'Loading inventory data...') ); content.appendChild(stack); return; } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } const lowStockVariants = variants.filter( v => v.inventoryItem?.tracked && v.inventoryQuantity <= REORDER_THRESHOLD ); const outOfStockVariants = variants.filter( v => v.inventoryItem?.tracked && v.inventoryQuantity <= 0 ); if (outOfStockVariants.length > 0) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, `${outOfStockVariants.length} variant(s) out of stock` ) ); } else if (lowStockVariants.length > 0) { stack.appendChild( root.createComponent(Banner, { tone: 'warning' }, `${lowStockVariants.length} variant(s) below reorder point` ) ); } else { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'All variants have healthy stock levels') ); } stack.appendChild(root.createComponent(Divider, {})); stack.appendChild( root.createComponent(Heading, { size: 'small' }, `Low Stock Details (≤${REORDER_THRESHOLD} units)`) ); lowStockVariants.forEach((variant) => { const variantBox = root.createComponent(Box, { padding: 'small', background: 'subdued', borderRadius: 'base' }); const variantStack = root.createComponent(BlockStack, { gap: 'extraTight' }); variantStack.appendChild(root.createComponent(Heading, { size: 'small' }, variant.title)); variantStack.appendChild( root.createComponent(Box, {}, `Stock: ${variant.inventoryQuantity} | Reorder at: ${REORDER_THRESHOLD}`) ); variantBox.appendChild(variantStack); stack.appendChild(variantBox); }); if (lowStockVariants.length === 0) { stack.appendChild(root.createComponent(Box, { padding: 'small' }, 'No variants need restocking')); } stack.appendChild( root.createComponent(Button, { onPress: fetchInventory }, 'Refresh Inventory') ); content.appendChild(stack); }; const adminBlock = root.createComponent(AdminBlock, { title: 'Inventory Alerts' }); adminBlock.appendChild(content); root.appendChild(adminBlock); fetchInventory(); root.mount(); } );
Anchor to Product details configuration ,[object Object]Product details configuration target
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.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add a configuration extension that lets merchants define required quantities for product bundles. This example demonstrates setting up component-level requirements for bundled products.
React
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> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.configuration.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let selectedLocations: string[] = ['warehouse-main']; let overwriteExisting = 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) { error = 'Please select at least one location'; updateUI(); return; } loading = true; error = null; updateUI(); const productId = api.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) { success = true; updateUI(); api.close(); } else { const errorData = await response.json(); error = errorData.message || 'Failed to sync inventory'; updateUI(); } } catch (err) { error = 'Connection error. Please try again.'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent( Button, { onPress: handleSync, disabled: loading || success }, loading ? 'Syncing...' : 'Sync Inventory' ) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Inventory synced successfully!') ); } if (error) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { title: 'Select warehouse locations', choices: locationOptions, value: selectedLocations, onChange: (value: string[]) => { selectedLocations = value; updateUI(); }, }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent( Checkbox, { checked: overwriteExisting, onChange: (value: boolean) => { overwriteExisting = value; updateUI(); }, }, 'Overwrite existing inventory levels' ) ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Sync Inventory from Warehouse', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add a configuration extension that lets merchants define pricing rules for product bundles, including component discounts and bundle-level adjustments.
React
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, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [publishing, setPublishing] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(''); const [product, setProduct] = useState(null); const [selectedMarketplaces, setSelectedMarketplaces] = useState(['amazon']); const [syncInventory, setSyncInventory] = useState(true); useEffect(() => { fetchProductDetails(); }, []); const fetchProductDetails = async () => { const productId = data.selected[0].id; try { const result = await query( `query GetProduct($id: ID!) { product(id: $id) { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } variants(first: 10) { nodes { id title sku inventoryQuantity } } } }`, { variables: { id: productId } } ); setProduct(result.data.product); } catch (err) { setError('Failed to load product details'); } finally { setLoading(false); } }; const handlePublish = async () => { setPublishing(true); setError(''); try { const response = await fetch('https://your-app.com/api/marketplace/publish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product, marketplaces: selectedMarketplaces, syncInventory, }), }); if (response.ok) { setSuccess(true); close(); } else { setError('Failed to publish to marketplace'); } } catch (err) { setError('Connection error. Please try again.'); } finally { setPublishing(false); } }; if (loading) { return ( <AdminAction title="Publish to Marketplace"> <Banner tone="info">Loading product details...</Banner> </AdminAction> ); } return ( <AdminAction title="Publish to Marketplace" primaryAction={ <Button onPress={handlePublish} disabled={publishing || success || !product}> {publishing ? 'Publishing...' : 'Publish'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && <Banner tone="success">Product published successfully!</Banner>} {error && <Banner tone="critical">{error}</Banner>} {product && ( <> <Box padding="base"> <BlockStack gap="tight"> <Banner tone="info"> {product.title} • {product.totalInventory} in stock • {product.priceRangeV2.minVariantPrice.amount} {product.priceRangeV2.minVariantPrice.currencyCode} </Banner> </BlockStack> </Box> <Divider /> <Box padding="base"> <ChoiceList title="Select marketplaces" choices={[ { id: 'amazon', label: 'Amazon' }, { id: 'ebay', label: 'eBay' }, { id: 'walmart', label: 'Walmart Marketplace' }, ]} value={selectedMarketplaces} onChange={setSelectedMarketplaces} /> </Box> <Divider /> <Box padding="base"> <Checkbox checked={syncInventory} onChange={setSyncInventory} > Keep inventory synced across marketplaces </Checkbox> </Box> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.configuration.render', async (root, api) => { let loading = true; let publishing = false; let success = false; let error = ''; let product: any = null; let selectedMarketplaces = ['amazon']; let syncInventory = true; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); if (loading) { content.appendChild( root.createComponent(Banner, { tone: 'info' }, 'Loading product details...') ); } else { const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Product published successfully!') ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } if (product) { const infoBox = root.createComponent(Box, { padding: 'base' }); infoBox.appendChild( root.createComponent( Banner, { tone: 'info' }, `${product.title} • ${product.totalInventory} in stock • ${product.priceRangeV2.minVariantPrice.amount} ${product.priceRangeV2.minVariantPrice.currencyCode}` ) ); stack.appendChild(infoBox); stack.appendChild(root.createComponent(Divider, {})); const marketplaceBox = root.createComponent(Box, { padding: 'base' }); marketplaceBox.appendChild( root.createComponent(ChoiceList, { title: 'Select marketplaces', choices: [ { id: 'amazon', label: 'Amazon' }, { id: 'ebay', label: 'eBay' }, { id: 'walmart', label: 'Walmart Marketplace' }, ], value: selectedMarketplaces, onChange: (value: string[]) => { selectedMarketplaces = value; updateUI(); }, }) ); stack.appendChild(marketplaceBox); stack.appendChild(root.createComponent(Divider, {})); const syncBox = root.createComponent(Box, { padding: 'base' }); syncBox.appendChild( root.createComponent( Checkbox, { checked: syncInventory, onChange: (checked: boolean) => { syncInventory = checked; updateUI(); }, }, 'Keep inventory synced across marketplaces' ) ); stack.appendChild(syncBox); } content.appendChild(stack); } primaryAction.appendChild( root.createComponent( Button, { onPress: handlePublish, disabled: publishing || success || !product }, publishing ? 'Publishing...' : 'Publish' ) ); }; const handlePublish = async () => { publishing = true; error = ''; updateUI(); try { const response = await fetch('https://your-app.com/api/marketplace/publish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product, marketplaces: selectedMarketplaces, syncInventory }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { error = 'Failed to publish to marketplace'; } } catch (err) { error = 'Connection error. Please try again.'; } finally { publishing = false; updateUI(); } }; const fetchProductDetails = async () => { const productId = api.data.selected[0].id; try { const result = await api.query( `query GetProduct($id: ID!) { product(id: $id) { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } variants(first: 10) { nodes { id title sku inventoryQuantity } } } }`, { variables: { id: productId } } ); product = result.data.product; } catch (err) { error = 'Failed to load product details'; } finally { loading = false; updateUI(); } }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Publish to Marketplace', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); await fetchProductDetails(); } );
Anchor to Product details print action ,[object Object]Product details print action target
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.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add an action extension that generates printable product labels by sending product data to your app backend. This example demonstrates calling your app's API to create customized label formats with barcode options and quantity settings.
React
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> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.print-action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let labelSize = ['standard']; let includeBarcode = true; let includePrice = true; const handleGenerateLabels = async () => { loading = true; error = null; updateUI(); const productId = api.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) { success = true; updateUI(); api.close(); } else { const errorData = await response.json(); error = errorData.message || 'Failed to generate labels'; updateUI(); } } catch (err) { error = 'Network error. Please try again.'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { primaryAction.replaceChildren( root.createComponent( Button, { onPress: handleGenerateLabels, disabled: loading || success }, loading ? 'Generating...' : 'Generate & Print' ) ); content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Labels generated! Opening print dialog...') ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { name: 'labelSize', title: 'Label Size', value: labelSize, onChange: (val: string[]) => { labelSize = val; updateUI(); }, choices: [ { id: 'small', label: 'Small (1" x 0.5")' }, { id: 'standard', label: 'Standard (2" x 1")' }, { id: 'large', label: 'Large (4" x 2")' }, ], }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent( Checkbox, { checked: includeBarcode, onChange: (val: boolean) => { includeBarcode = val; } }, 'Include barcode' ) ); innerStack.appendChild( root.createComponent( Checkbox, { checked: includePrice, onChange: (val: boolean) => { includePrice = val; } }, 'Include price' ) ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Generate Product Labels', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that generates a printable product specification sheet with detailed attributes, dimensions, and compliance information using the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access) to fetch complete product data including metafields and variant specifications.
React
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.print-action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(false); const [product, setProduct] = useState(null); const [error, setError] = useState(''); const [labelSize, setLabelSize] = useState(['standard']); const [includeBarcode, setIncludeBarcode] = useState(true); const [includePrice, setIncludePrice] = useState(true); useEffect(() => { fetchProductDetails(); }, []); const fetchProductDetails = async () => { setLoading(true); try { const productId = data.selected[0].id; const result = await query( `query GetProduct($id: ID!) { product(id: $id) { title vendor productType variants(first: 10) { edges { node { id title sku barcode price inventoryQuantity } } } } }`, { variables: { id: productId } } ); if (result.data?.product) { setProduct(result.data.product); } else { setError('Failed to load product details'); } } catch (err) { setError('Error fetching product data'); } finally { setLoading(false); } }; const handlePrint = () => { const printData = { product, options: { labelSize: labelSize[0], includeBarcode, includePrice }, }; console.log('Printing label:', printData); window.print(); close(); }; const variantCount = product?.variants?.edges?.length || 0; return ( <AdminAction title="Print Product Label" primaryAction={ <Button onPress={handlePrint} disabled={loading || !product}> {loading ? 'Loading...' : 'Print Label'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {error && <Banner tone="critical">{error}</Banner>} {product && ( <Banner tone="info"> {product.title} - {variantCount} variant(s) available </Banner> )} <Divider /> <ChoiceList title="Label Size" name="labelSize" value={labelSize} onChange={setLabelSize} choices={[ { label: 'Standard (2" x 1")', value: 'standard' }, { label: 'Large (4" x 2")', value: 'large' }, { label: 'Shelf Tag (3" x 1.5")', value: 'shelf' }, ]} /> <Divider /> <BlockStack gap="base"> <Checkbox checked={includeBarcode} onChange={setIncludeBarcode} > Include barcode/SKU </Checkbox> <Checkbox checked={includePrice} onChange={setIncludePrice} > Include pricing </Checkbox> </BlockStack> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.print-action.render', (root, api) => { let loading = true; let product: any = null; let error = ''; let labelSize = ['standard']; let includeBarcode = true; let includePrice = true; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); const variantCount = product?.variants?.edges?.length || 0; primaryAction.appendChild( root.createComponent( Button, { onPress: handlePrint, disabled: loading || !product }, loading ? 'Loading...' : 'Print Label' ) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (error) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } if (product) { stack.appendChild( root.createComponent( Banner, { tone: 'info' }, `${product.title} - ${variantCount} variant(s) available` ) ); } stack.appendChild(root.createComponent(Divider, {})); stack.appendChild( root.createComponent(ChoiceList, { title: 'Label Size', name: 'labelSize', value: labelSize, onChange: (val: string[]) => { labelSize = val; updateUI(); }, choices: [ { label: 'Standard (2" x 1")', value: 'standard' }, { label: 'Large (4" x 2")', value: 'large' }, { label: 'Shelf Tag (3" x 1.5")', value: 'shelf' }, ], }) ); stack.appendChild(root.createComponent(Divider, {})); const checkboxStack = root.createComponent(BlockStack, { gap: 'base' }); checkboxStack.appendChild( root.createComponent( Checkbox, { checked: includeBarcode, onChange: (val: boolean) => { includeBarcode = val; updateUI(); } }, 'Include barcode/SKU' ) ); checkboxStack.appendChild( root.createComponent( Checkbox, { checked: includePrice, onChange: (val: boolean) => { includePrice = val; updateUI(); } }, 'Include pricing' ) ); stack.appendChild(checkboxStack); content.appendChild(stack); }; const handlePrint = () => { const printData = { product, options: { labelSize: labelSize[0], includeBarcode, includePrice }, }; console.log('Printing label:', printData); api.close(); }; const fetchProductDetails = async () => { try { const productId = api.data.selected[0].id; const result = await api.query( `query GetProduct($id: ID!) { product(id: $id) { title vendor productType variants(first: 10) { edges { node { id title sku barcode price inventoryQuantity } } } } }`, { variables: { id: productId } } ); if (result.data?.product) { product = result.data.product; } else { error = 'Failed to load product details'; } } catch (err) { error = 'Error fetching product data'; } finally { loading = false; updateUI(); } }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Print Product Label', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); fetchProductDetails(); } );
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.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Add a should-render extension that checks with your app backend whether a product has printable labels configured before showing the print action. This example demonstrates calling your app's API to determine if the print action should be available based on custom business logic.
React
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 }; } });TS
import { extension } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.print-action.should-render', async (root, 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 }; } } );Description
Create a should-render extension that conditionally shows the print action only for products that have inventory tracking enabled and stock levels, using the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access) to query product inventory data from the [GraphQL Admin API](/docs/api/admin-graphql).
React
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; try { const result = await api.query( `query GetProductInventory($id: ID!) { product(id: $id) { tracksInventory totalInventory variants(first: 10) { nodes { inventoryQuantity inventoryItem { tracked } } } } }`, { variables: { id: productId } } ); const product = result?.data?.product; if (!product) { return { render: false }; } // Only show print action for products with inventory tracking const hasTrackedInventory = product.tracksInventory; const hasStock = product.totalInventory > 0; const hasTrackedVariants = product.variants.nodes.some( (variant) => variant.inventoryItem?.tracked ); // Show print action if product tracks inventory and has stock return { render: hasTrackedInventory && hasStock && hasTrackedVariants }; } catch (err) { console.error('Failed to check product inventory:', err); return { render: false }; } });TS
import { extension } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.print-action.should-render', async (root, api) => { const productId = api.data.selected[0].id; try { const result = await api.query( `query GetProductInventory($id: ID!) { product(id: $id) { tracksInventory totalInventory variants(first: 10) { nodes { inventoryQuantity inventoryItem { tracked } } } } }`, { variables: { id: productId } } ); const product = result?.data?.product; if (!product) { return { render: false }; } // Only show print action for products with inventory tracking const hasTrackedInventory = product.tracksInventory; const hasStock = product.totalInventory > 0; const hasTrackedVariants = product.variants.nodes.some( (variant: { inventoryItem?: { tracked: boolean } }) => variant.inventoryItem?.tracked ); // Show print action if product tracks inventory and has stock return { render: hasTrackedInventory && hasStock && hasTrackedVariants }; } catch (err) { console.error('Failed to check product inventory:', err); return { render: false }; } } );
Anchor to Product details reorder ,[object Object]Product details reorder target
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.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add a reorder extension that lets merchants arrange the display order of product variants. This example demonstrates managing variant sequence using drag-and-drop or manual reordering.
React
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> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.reorder.render', (root, api) => { let loading = false; let success = false; let error = ''; let selectedLocations = ['warehouse-main']; let overwriteExisting = 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) { error = 'Please select at least one location'; updateUI(); return; } loading = true; error = ''; updateUI(); const productId = api.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) { success = true; updateUI(); api.close(); } else { const errorData = await response.json(); error = errorData.message || 'Failed to sync inventory'; updateUI(); } } catch (err) { error = 'Connection error. Please try again.'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Inventory synced successfully!') ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } stack.appendChild( root.createComponent(Box, {}, root.createComponent(ChoiceList, { title: 'Select warehouse locations', choices: locations, value: selectedLocations, onChange: (val) => { selectedLocations = val; updateUI(); }, }) ) ); stack.appendChild(root.createComponent(Divider, {})); stack.appendChild( root.createComponent(Checkbox, { checked: overwriteExisting, onChange: (val) => { overwriteExisting = val; updateUI(); }, }, 'Overwrite existing inventory levels') ); content.appendChild(stack); primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent(Button, { onPress: handleSync, disabled: loading || success, }, loading ? 'Syncing...' : 'Sync Inventory') ); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Sync Inventory from Warehouse', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add a reorder extension that lets merchants arrange the display order of product images and videos. This example demonstrates managing media sequence.
React
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.reorder.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(false); const [fetching, setFetching] = useState(true); const [success, setSuccess] = useState(false); const [error, setError] = useState(''); const [product, setProduct] = useState(null); const [selectedMarketplaces, setSelectedMarketplaces] = useState(['amazon']); const [syncInventory, setSyncInventory] = useState(true); useEffect(() => { fetchProductDetails(); }, []); const fetchProductDetails = async () => { const productId = data.selected[0].id; try { const result = await query( `query GetProduct($id: ID!) { product(id: $id) { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } variants(first: 10) { nodes { id title sku inventoryQuantity } } } }`, { variables: { id: productId } } ); setProduct(result?.data?.product); } catch (err) { setError('Failed to fetch product details'); } finally { setFetching(false); } }; const handlePublish = async () => { setLoading(true); setError(''); try { const response = await fetch('https://your-app.com/api/publish-marketplace', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productId: product.id, marketplaces: selectedMarketplaces, syncInventory, variants: product.variants.nodes, }), }); if (response.ok) { setSuccess(true); close(); } else { setError('Failed to publish product'); } } catch (err) { setError('Connection error. Please try again.'); } finally { setLoading(false); } }; if (fetching) { return ( <AdminAction title="Publish to Marketplace"> <Banner tone="info">Loading product details...</Banner> </AdminAction> ); } return ( <AdminAction title="Publish to Marketplace" primaryAction={ <Button onPress={handlePublish} disabled={loading || success || !product}> {loading ? 'Publishing...' : 'Publish'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && <Banner tone="success">Product published successfully!</Banner>} {error && <Banner tone="critical">{error}</Banner>} {product && ( <> <Box padding="base"> <BlockStack gap="tight"> <Banner tone="info"> {product.title} • {product.totalInventory} in stock • {product.priceRangeV2?.minVariantPrice?.amount} {product.priceRangeV2?.minVariantPrice?.currencyCode} </Banner> </BlockStack> </Box> <Divider /> <Box padding="base"> <ChoiceList title="Select Marketplaces" choices={[ { id: 'amazon', label: 'Amazon' }, { id: 'ebay', label: 'eBay' }, { id: 'walmart', label: 'Walmart' }, ]} value={selectedMarketplaces} onChange={setSelectedMarketplaces} /> </Box> <Divider /> <Box padding="base"> <Checkbox checked={syncInventory} onChange={setSyncInventory}> Sync inventory levels automatically </Checkbox> </Box> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-details.reorder.render', async (root, api) => { let loading = false; let success = false; let error = ''; let product: any = null; let selectedMarketplaces = ['amazon']; let syncInventory = true; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); if (success) { content.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Product published successfully!') ); } if (error) { content.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } if (product) { const stack = root.createComponent(BlockStack, { gap: 'base' }); const infoBox = root.createComponent(Box, { padding: 'base' }); infoBox.appendChild( root.createComponent( Banner, { tone: 'info' }, `${product.title} • ${product.totalInventory} in stock • ${product.priceRangeV2?.minVariantPrice?.amount} ${product.priceRangeV2?.minVariantPrice?.currencyCode}` ) ); stack.appendChild(infoBox); stack.appendChild(root.createComponent(Divider, {})); const marketplaceBox = root.createComponent(Box, { padding: 'base' }); marketplaceBox.appendChild( root.createComponent(ChoiceList, { title: 'Select Marketplaces', choices: [ { id: 'amazon', label: 'Amazon' }, { id: 'ebay', label: 'eBay' }, { id: 'walmart', label: 'Walmart' }, ], value: selectedMarketplaces, onChange: (value: string[]) => { selectedMarketplaces = value; updateUI(); }, }) ); stack.appendChild(marketplaceBox); stack.appendChild(root.createComponent(Divider, {})); const syncBox = root.createComponent(Box, { padding: 'base' }); syncBox.appendChild( root.createComponent( Checkbox, { checked: syncInventory, onChange: (checked: boolean) => { syncInventory = checked; updateUI(); }, }, 'Sync inventory levels automatically' ) ); stack.appendChild(syncBox); content.appendChild(stack); } primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent( Button, { onPress: handlePublish, disabled: loading || success || !product }, loading ? 'Publishing...' : 'Publish' ) ); }; const handlePublish = async () => { loading = true; error = ''; updateUI(); try { const response = await fetch('https://your-app.com/api/publish-marketplace', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productId: product.id, marketplaces: selectedMarketplaces, syncInventory, variants: product.variants.nodes, }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { error = 'Failed to publish product'; } } catch (err) { error = 'Connection error. Please try again.'; } finally { loading = false; updateUI(); } }; // Fetch product details using direct API const productId = api.data.selected[0].id; try { const result = await api.query( `query GetProduct($id: ID!) { product(id: $id) { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } variants(first: 10) { nodes { id title sku inventoryQuantity } } } }`, { variables: { id: productId } } ); product = result?.data?.product; } catch (err) { error = 'Failed to fetch product details'; } secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Publish to Marketplace', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
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.
Anchor to Product index action ,[object Object]Product index action target
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.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add an action extension on the products index page to sync inventory levels for multiple products at once from an external warehouse. This example demonstrates batch inventory updates.
React
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> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-index.action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let warehouse = ['main']; let overwriteZero = false; const selectedCount = api.data.selected.length; const handleSync = async () => { loading = true; error = null; updateUI(); const productIds = api.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) { success = true; updateUI(); api.close(); } else { const errorData = await response.json(); error = errorData.message || 'Failed to sync inventory'; updateUI(); } } catch (err) { error = 'Connection error. Please try again.'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { primaryAction.replaceChildren( root.createComponent( Button, { onPress: handleSync, disabled: loading || success || selectedCount === 0 }, loading ? 'Syncing...' : `Sync ${selectedCount} Product${selectedCount !== 1 ? 's' : ''}` ) ); content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, `Inventory synced successfully for ${selectedCount} product(s)!` ) ); } if (error) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { name: 'warehouse', title: 'Select Warehouse', value: warehouse, onChange: (val: string[]) => { warehouse = val; updateUI(); }, choices: [ { label: 'Main Warehouse (US)', id: 'main' }, { label: 'East Coast Fulfillment', id: 'east' }, { label: 'West Coast Fulfillment', id: 'west' }, ], }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent( Checkbox, { checked: overwriteZero, onChange: (val: boolean) => { overwriteZero = val; updateUI(); }, }, 'Update products even if warehouse shows zero stock' ) ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Sync Inventory from Warehouse', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that publishes selected products to an external marketplace. This example demonstrates using the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access) to fetch product details including variants and inventory before syncing to a third-party sales channel.
React
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.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [publishing, setPublishing] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(''); const [products, setProducts] = useState([]); const [marketplace, setMarketplace] = useState(['amazon']); const [includeInventory, setIncludeInventory] = useState(true); useEffect(() => { fetchProductDetails(); }, []); const fetchProductDetails = async () => { try { const selectedIds = data.selected.map(item => item.id); const results = await Promise.all( selectedIds.map(id => query(`query GetProduct($id: ID!) { product(id: $id) { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } } }`, { variables: { id } }) ) ); const productData = results .filter(r => r.data?.product) .map(r => r.data.product); setProducts(productData); } catch (err) { setError('Failed to load product details'); } finally { setLoading(false); } }; const handlePublish = async () => { setPublishing(true); setError(''); try { const response = await fetch('https://your-app.com/api/publish-marketplace', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ products, marketplaces: marketplace, includeInventory, }), }); if (response.ok) { setSuccess(true); close(); } else { setError('Failed to publish products'); } } catch (err) { setError('Connection error. Please try again.'); } finally { setPublishing(false); } }; if (loading) { return ( <AdminAction title="Publish to Marketplace"> <Banner tone="info">Loading product details...</Banner> </AdminAction> ); } return ( <AdminAction title="Publish to Marketplace" primaryAction={ <Button onPress={handlePublish} disabled={publishing || success || products.length === 0}> {publishing ? 'Publishing...' : `Publish ${products.length} Product(s)`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && <Banner tone="success">Products published successfully!</Banner>} {error && <Banner tone="critical">{error}</Banner>} <Box padding="base"> <BlockStack gap="base"> <ChoiceList title="Select Marketplace" choices={[ { id: 'amazon', label: 'Amazon' }, { id: 'ebay', label: 'eBay' }, { id: 'walmart', label: 'Walmart' }, ]} value={marketplace} onChange={setMarketplace} /> <Divider /> <Checkbox checked={includeInventory} onChange={setIncludeInventory} > Sync inventory levels </Checkbox> </BlockStack> </Box> <Divider /> <Box padding="base"> <Banner tone="info"> {products.length} product(s) selected with total inventory of{' '} {products.reduce((sum, p) => sum + (p.totalInventory || 0), 0)} units </Banner> </Box> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-index.action.render', async (root, api) => { const { data, close, query } = api; let products: any[] = []; let marketplace = ['amazon']; let includeInventory = true; let publishing = false; let success = false; let errorMsg = ''; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Products published successfully!') ); } if (errorMsg) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, errorMsg) ); } const optionsBox = root.createComponent(Box, { padding: 'base' }); const optionsStack = root.createComponent(BlockStack, { gap: 'base' }); optionsStack.appendChild( root.createComponent(ChoiceList, { title: 'Select Marketplace', choices: [ { id: 'amazon', label: 'Amazon' }, { id: 'ebay', label: 'eBay' }, { id: 'walmart', label: 'Walmart' }, ], value: marketplace, onChange: (val: string[]) => { marketplace = val; updateUI(); }, }) ); optionsStack.appendChild(root.createComponent(Divider, {})); optionsStack.appendChild( root.createComponent(Checkbox, { checked: includeInventory, onChange: (val: boolean) => { includeInventory = val; updateUI(); }, }, 'Sync inventory levels') ); optionsBox.appendChild(optionsStack); stack.appendChild(optionsBox); stack.appendChild(root.createComponent(Divider, {})); const totalInventory = products.reduce((sum, p) => sum + (p.totalInventory || 0), 0); const infoBox = root.createComponent(Box, { padding: 'base' }); infoBox.appendChild( root.createComponent(Banner, { tone: 'info' }, `${products.length} product(s) selected with total inventory of ${totalInventory} units` ) ); stack.appendChild(infoBox); content.appendChild(stack); primaryAction.appendChild( root.createComponent(Button, { onPress: handlePublish, disabled: publishing || success || products.length === 0, }, publishing ? 'Publishing...' : `Publish ${products.length} Product(s)`) ); }; const handlePublish = async () => { publishing = true; errorMsg = ''; updateUI(); try { const response = await fetch('https://your-app.com/api/publish-marketplace', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ products, marketplaces: marketplace, includeInventory }), }); if (response.ok) { success = true; updateUI(); close(); } else { errorMsg = 'Failed to publish products'; } } catch (err) { errorMsg = 'Connection error. Please try again.'; } finally { publishing = false; updateUI(); } }; // Fetch product details using direct API const selectedIds = data.selected.map(item => item.id); const results = await Promise.all( selectedIds.map(id => query(`query GetProduct($id: ID!) { product(id: $id) { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } } }`, { variables: { id } }) ) ); products = results.filter(r => r.data?.product).map(r => r.data.product); secondaryAction.appendChild( root.createComponent(Button, { onPress: () => close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Publish to Marketplace', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
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.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Add a should-render extension that checks with your app backend whether selected products belong to categories approved for bulk operations. This example demonstrates calling your app's API to validate product eligibility based on custom business rules stored in your backend.
React
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}; } } );TS
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}; } } );Description
Add an action extension that only displays on the products index page when the store has significant inventory. This example checks total store inventory levels before showing the action.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-index.action.should-render', async ({data, query}) => { const productId = data.selected[0].id; try { const {data: responseData, errors} = await query( `query GetProductInventory($id: ID!) { product(id: $id) { totalInventory status variants(first: 10) { nodes { inventoryQuantity } } } }`, {variables: {id: productId}} ); if (errors || !responseData?.product) { return {display: false}; } const product = responseData.product; // Only show action for active products with inventory in stock const hasStock = product.totalInventory > 0; const isActive = product.status === 'ACTIVE'; return {display: hasStock && isActive}; } catch (err) { console.error('Failed to check inventory status:', err); return {display: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-index.action.should-render', async ({data, query}) => { const productId = data.selected[0].id; try { const {data: responseData, errors} = await query( `query GetProductInventory($id: ID!) { product(id: $id) { totalInventory status variants(first: 10) { nodes { inventoryQuantity } } } }`, {variables: {id: productId}} ); if (errors || !responseData?.product) { return {display: false}; } const product = responseData.product; // Only show action for active products with inventory in stock const hasStock = product.totalInventory > 0; const isActive = product.status === 'ACTIVE'; return {display: hasStock && isActive}; } catch (err) { console.error('Failed to check inventory status:', err); return {display: false}; } } );
Anchor to Product index selection action ,[object Object]Product index selection action target
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.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add an action extension that exports selected products to a CSV file or external system. This example demonstrates processing multiple selected products and generating export data.
React
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> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-index.selection-action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let warehouse = ['main']; let overwriteExisting = true; const selectedCount = api.data.selected.length; const handleSync = async () => { loading = true; error = null; updateUI(); const productIds = api.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) { success = true; updateUI(); api.close(); } else { const errorData = await response.json(); error = errorData.message || 'Failed to sync inventory'; updateUI(); } } catch (err) { error = 'Connection error. Please try again.'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { primaryAction.replaceChildren( root.createComponent( Button, { onPress: handleSync, disabled: loading || success }, loading ? 'Syncing...' : `Sync ${selectedCount} Products` ) ); content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent( Banner, { tone: 'success' }, `Inventory synced successfully for ${selectedCount} products!` ) ); } if (error) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { name: 'warehouse', title: 'Select Warehouse', value: warehouse, onChange: (val: string[]) => { warehouse = val; updateUI(); }, choices: [ { label: 'Main Warehouse', id: 'main' }, { label: 'East Coast Fulfillment', id: 'east' }, { label: 'West Coast Fulfillment', id: 'west' }, ], }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent( Checkbox, { checked: overwriteExisting, onChange: (val: boolean) => { overwriteExisting = val; updateUI(); }, }, 'Overwrite existing inventory levels' ) ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Sync Inventory from Warehouse', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that applies tags to multiple selected products at once. This example demonstrates batch tagging operations using GraphQL mutations.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, 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, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [publishing, setPublishing] = useState(false); const [products, setProducts] = useState([]); const [selectedIds, setSelectedIds] = useState([]); const [success, setSuccess] = useState(false); const [error, setError] = useState(''); useEffect(() => { fetchProductDetails(); }, []); const fetchProductDetails = async () => { const productIds = data.selected.map(item => item.id); try { const result = await query( `query GetProducts($ids: [ID!]!) { nodes(ids: $ids) { ... on Product { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } } } }`, { variables: { ids: productIds } } ); const fetchedProducts = result.data.nodes.filter(Boolean); setProducts(fetchedProducts); setSelectedIds(fetchedProducts.map(p => p.id)); } catch (err) { setError('Failed to fetch product details'); } finally { setLoading(false); } }; const toggleProduct = (productId) => { setSelectedIds(prev => prev.includes(productId) ? prev.filter(id => id !== productId) : [...prev, productId] ); }; const handlePublish = async () => { setPublishing(true); try { // Simulate marketplace API call await new Promise(resolve => setTimeout(resolve, 1500)); setSuccess(true); close(); } catch (err) { setError('Failed to publish products'); } finally { setPublishing(false); } }; if (loading) { return ( <AdminAction title="Publish to Marketplace"> <Banner tone="info">Loading product details...</Banner> </AdminAction> ); } return ( <AdminAction title="Publish to Marketplace" primaryAction={ <Button onPress={handlePublish} disabled={publishing || success || selectedIds.length === 0}> {publishing ? 'Publishing...' : `Publish ${selectedIds.length} Products`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && <Banner tone="success">Products published successfully!</Banner>} {error && <Banner tone="critical">{error}</Banner>} <Banner tone="info"> Select products to publish to your external marketplace </Banner> <Divider /> <BlockStack gap="base"> {products.map(product => ( <Box key={product.id} padding="small"> <Checkbox checked={selectedIds.includes(product.id)} onChange={() => toggleProduct(product.id)} > {product.title} - {product.priceRangeV2?.minVariantPrice?.amount} {product.priceRangeV2?.minVariantPrice?.currencyCode} ({product.totalInventory} in stock) </Checkbox> </Box> ))} </BlockStack> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-index.selection-action.render', async (root, api) => { const { data, close, query } = api; let products = []; let selectedIds = []; let publishing = false; let success = false; let error = ''; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); if (success) { content.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Products published successfully!') ); } if (error) { content.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } const stack = root.createComponent(BlockStack, { gap: 'base' }); stack.appendChild( root.createComponent(Banner, { tone: 'info' }, 'Select products to publish to your external marketplace') ); stack.appendChild(root.createComponent(Divider, {})); const productStack = root.createComponent(BlockStack, { gap: 'base' }); products.forEach(product => { const box = root.createComponent(Box, { padding: 'small' }); const price = product.priceRangeV2?.minVariantPrice; const label = `${product.title} - ${price?.amount} ${price?.currencyCode} (${product.totalInventory} in stock)`; box.appendChild( root.createComponent( Checkbox, { checked: selectedIds.includes(product.id), onChange: () => { if (selectedIds.includes(product.id)) { selectedIds = selectedIds.filter(id => id !== product.id); } else { selectedIds = [...selectedIds, product.id]; } updateUI(); }, }, label ) ); productStack.appendChild(box); }); stack.appendChild(productStack); content.appendChild(stack); primaryAction.appendChild( root.createComponent( Button, { onPress: handlePublish, disabled: publishing || success || selectedIds.length === 0, }, publishing ? 'Publishing...' : `Publish ${selectedIds.length} Products` ) ); }; const handlePublish = async () => { publishing = true; updateUI(); try { await new Promise(resolve => setTimeout(resolve, 1500)); success = true; updateUI(); close(); } catch (err) { error = 'Failed to publish products'; updateUI(); } finally { publishing = false; updateUI(); } }; // Fetch product details const productIds = data.selected.map(item => item.id); try { const result = await query( `query GetProducts($ids: [ID!]!) { nodes(ids: $ids) { ... on Product { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } } } }`, { variables: { ids: productIds } } ); products = result.data.nodes.filter(Boolean); selectedIds = products.map(p => p.id); } catch (err) { error = 'Failed to fetch product details'; } secondaryAction.appendChild( root.createComponent(Button, { onPress: () => close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Publish to Marketplace', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
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.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Add a should-render extension that checks your app backend to determine if all selected products are registered in your app's catalog system. This validates bulk action eligibility.
React
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}; } } );TS
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}; } } );Description
Add a should-render extension that checks if all selected products have available inventory. This example validates that bulk actions only appear when all selected items meet the criteria.
React
import {extension} from '@shopify/ui-extensions/admin'; const TARGET = 'admin.product-index.selection-action.should-render'; export default extension(TARGET, async ({data, query}) => { const productId = data.selected[0].id; try { const {data: productData, errors} = await query( `query GetProductInventory($id: ID!) { product(id: $id) { totalInventory status variants(first: 10) { nodes { inventoryQuantity } } } }`, {variables: {id: productId}} ); if (errors || !productData?.product) { return {display: false}; } const product = productData.product; const hasStock = product.totalInventory > 0; const isActive = product.status === 'ACTIVE'; // Only show action for active products with available inventory return {display: hasStock && isActive}; } catch (err) { console.error('Failed to check inventory status:', err); return {display: false}; } });TS
import {extension} from '@shopify/ui-extensions/admin'; const TARGET = 'admin.product-index.selection-action.should-render'; export default extension(TARGET, async ({data, query}) => { const productId = data.selected[0].id; try { const {data: productData, errors} = await query( `query GetProductInventory($id: ID!) { product(id: $id) { totalInventory status variants(first: 10) { nodes { inventoryQuantity } } } }`, {variables: {id: productId}} ); if (errors || !productData?.product) { return {display: false}; } const product = productData.product; const hasStock = product.totalInventory > 0; const isActive = product.status === 'ACTIVE'; // Only show action for active products with available inventory return {display: hasStock && isActive}; } catch (err) { console.error('Failed to check inventory status:', err); 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.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add a print action extension that generates barcode labels for selected products. This example demonstrates fetching SKU data and rendering printable barcode labels.
React
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> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-index.selection-print-action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let warehouse = ['main']; let overwriteExisting = true; let syncCount = 0; const selectedProducts = api.data.selected.map(item => item.id); const handleSync = async () => { loading = true; error = null; updateUI(); 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(); syncCount = result.updatedCount; success = true; updateUI(); api.close(); } catch (err) { error = err instanceof Error ? err.message : 'Sync failed'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { primaryAction.replaceChildren( root.createComponent( Button, { onPress: handleSync, disabled: loading || success }, loading ? 'Syncing...' : `Sync ${selectedProducts.length} Products` ) ); content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, `Successfully synced ${syncCount} inventory levels!` ) ); } if (error) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { name: 'warehouse', title: 'Select Warehouse', value: warehouse, onChange: (val: string[]) => { warehouse = val; updateUI(); }, choices: [ { label: 'Main Warehouse', id: 'main' }, { label: 'East Coast Fulfillment', id: 'east' }, { label: 'West Coast Fulfillment', id: 'west' }, ], }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent( Checkbox, { checked: overwriteExisting, onChange: (val: boolean) => { overwriteExisting = val; updateUI(); }, }, 'Overwrite existing inventory levels' ) ); box.appendChild(innerStack); stack.appendChild(box); stack.appendChild( root.createComponent(Banner, { tone: 'info' }, `${selectedProducts.length} product(s) selected for inventory sync` ) ); content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Sync Inventory from Warehouse', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that publishes selected products to an external marketplace using the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access) to fetch complete product details including variants, images, and pricing before syncing.
React
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, query } = useApi(TARGET); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState<string | null>(null); const [products, setProducts] = useState<any[]>([]); const [marketplace, setMarketplace] = useState(['amazon']); const [includeImages, setIncludeImages] = useState(true); const [includeInventory, setIncludeInventory] = useState(true); useEffect(() => { fetchProductDetails(); }, []); const fetchProductDetails = async () => { setLoading(true); try { const productIds = data.selected.map((item) => item.id); const response = await query( `query GetProducts($ids: [ID!]!) { nodes(ids: $ids) { ... on Product { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } featuredImage { url } variantsCount { count } } } }`, { variables: { ids: productIds } } ); if (response.data?.nodes) { setProducts(response.data.nodes.filter(Boolean)); } } catch (err) { setError('Failed to load product details'); } finally { setLoading(false); } }; const handlePublish = async () => { setLoading(true); setError(null); try { // Simulate marketplace API call await new Promise((resolve) => setTimeout(resolve, 1500)); setSuccess(true); close(); } catch (err) { setError('Failed to publish products'); } finally { setLoading(false); } }; return ( <AdminAction title="Publish to Marketplace" primaryAction={ <Button onPress={handlePublish} disabled={loading || success || products.length === 0}> {loading ? 'Publishing...' : `Publish ${products.length} Products`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && <Banner tone="success">Products published successfully!</Banner>} {error && <Banner tone="critical">{error}</Banner>} <Box padding="base"> <BlockStack gap="base"> <ChoiceList title="Select Marketplace" name="marketplace" value={marketplace} onChange={setMarketplace} choices={[ { label: 'Amazon', value: 'amazon' }, { label: 'eBay', value: 'ebay' }, { label: 'Walmart', value: 'walmart' }, ]} /> <Divider /> <Checkbox checked={includeImages} onChange={setIncludeImages} > Include product images </Checkbox> <Checkbox checked={includeInventory} onChange={setIncludeInventory} > Sync inventory levels </Checkbox> <Divider /> <Banner tone="info"> {products.length} products selected with {products.reduce((sum, p) => sum + (p.variantsCount?.count || 0), 0)} total variants </Banner> </BlockStack> </Box> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-index.selection-print-action.render', async (root, api) => { let loading = true; let success = false; let error: string | null = null; let products: any[] = []; let marketplace = ['amazon']; let includeImages = true; let includeInventory = true; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Products published successfully!') ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { title: 'Select Marketplace', name: 'marketplace', value: marketplace, onChange: (val: string[]) => { marketplace = val; updateUI(); }, choices: [ { label: 'Amazon', value: 'amazon' }, { label: 'eBay', value: 'ebay' }, { label: 'Walmart', value: 'walmart' }, ], }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent(Checkbox, { checked: includeImages, onChange: (val: boolean) => { includeImages = val; updateUI(); }, }, 'Include product images') ); innerStack.appendChild( root.createComponent(Checkbox, { checked: includeInventory, onChange: (val: boolean) => { includeInventory = val; updateUI(); }, }, 'Sync inventory levels') ); innerStack.appendChild(root.createComponent(Divider, {})); const variantCount = products.reduce((sum, p) => sum + (p.variantsCount?.count || 0), 0); innerStack.appendChild( root.createComponent(Banner, { tone: 'info' }, `${products.length} products selected with ${variantCount} total variants` ) ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent( Button, { onPress: handlePublish, disabled: loading || success || products.length === 0 }, loading ? 'Publishing...' : `Publish ${products.length} Products` ) ); }; const handlePublish = async () => { loading = true; error = null; updateUI(); try { await new Promise((resolve) => setTimeout(resolve, 1500)); success = true; updateUI(); api.close(); } catch (err) { error = 'Failed to publish products'; updateUI(); } finally { loading = false; updateUI(); } }; // Fetch product details using direct API const productIds = api.data.selected.map((item) => item.id); const response = await api.query( `query GetProducts($ids: [ID!]!) { nodes(ids: $ids) { ... on Product { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } variantsCount { count } } } }`, { variables: { ids: productIds } } ); if (response.data?.nodes) { products = response.data.nodes.filter(Boolean); } loading = false; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Publish to Marketplace', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
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.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Add a should-render extension that shows the print action only for products that have barcodes configured. This example demonstrates checking product metafields to conditionally display the print option.
React
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 }; } });TS
import { extension } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-index.selection-print-action.should-render', async (root, api) => { const { data, auth } = api; const selectedIds = data.selected.map((item: { id: string }) => 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 }; } } );Description
Add an action extension that checks product eligibility for marketplace publishing using the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access) to fetch product details including inventory status, pricing, and publication requirements before allowing bulk listing.
React
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.should-render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [publishing, setPublishing] = useState(false); const [products, setProducts] = useState([]); const [selectedMarketplaces, setSelectedMarketplaces] = useState(['amazon']); const [includeVariants, setIncludeVariants] = useState(true); const [error, setError] = useState(''); const [success, setSuccess] = useState(false); useEffect(() => { fetchProductDetails(); }, []); const fetchProductDetails = async () => { try { const productIds = data.selected.map(item => item.id); const response = await query(` query GetProducts($ids: [ID!]!) { nodes(ids: $ids) { ... on Product { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } featuredImage { url } } } } `, { variables: { ids: productIds } }); const validProducts = response.data.nodes.filter(p => p !== null); setProducts(validProducts); } catch (err) { setError('Failed to load product details'); } finally { setLoading(false); } }; const handlePublish = async () => { setPublishing(true); const eligibleProducts = products.filter(p => p.status === 'ACTIVE' && p.totalInventory > 0 && p.featuredImage ); if (eligibleProducts.length === 0) { setError('No eligible products to publish'); setPublishing(false); return; } // Simulate marketplace API call await new Promise(resolve => setTimeout(resolve, 1500)); setSuccess(true); close(); }; const eligibleCount = products.filter(p => p.status === 'ACTIVE' && p.totalInventory > 0 && p.featuredImage ).length; if (loading) { return ( <AdminAction title="Publish to Marketplace"> <Banner tone="info">Loading product details...</Banner> </AdminAction> ); } return ( <AdminAction title="Publish to Marketplace" primaryAction={ <Button onPress={handlePublish} disabled={publishing || success || eligibleCount === 0}> {publishing ? 'Publishing...' : `Publish ${eligibleCount} Products`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && <Banner tone="success">Products published successfully!</Banner>} {error && <Banner tone="critical">{error}</Banner>} {eligibleCount < products.length && ( <Banner tone="warning"> {products.length - eligibleCount} products ineligible (inactive, no inventory, or missing image) </Banner> )} <Box padding="base"> <ChoiceList title="Select Marketplaces" choices={[ { label: 'Amazon', id: 'amazon' }, { label: 'eBay', id: 'ebay' }, { label: 'Google Shopping', id: 'google' }, ]} value={selectedMarketplaces} onChange={setSelectedMarketplaces} /> </Box> <Divider /> <Box padding="base"> <Checkbox checked={includeVariants} onChange={setIncludeVariants} > Include all product variants </Checkbox> </Box> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-index.selection-print-action.should-render', async (root, api) => { let publishing = false; let success = false; let error = ''; let products: any[] = []; let selectedMarketplaces = ['amazon']; let includeVariants = true; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const getEligibleCount = () => products.filter(p => p.status === 'ACTIVE' && p.totalInventory > 0 && p.featuredImage ).length; const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); const eligibleCount = getEligibleCount(); primaryAction.appendChild( root.createComponent( Button, { onPress: handlePublish, disabled: publishing || success || eligibleCount === 0 }, publishing ? 'Publishing...' : `Publish ${eligibleCount} Products` ) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Products published successfully!') ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } if (eligibleCount < products.length) { stack.appendChild( root.createComponent( Banner, { tone: 'warning' }, `${products.length - eligibleCount} products ineligible (inactive, no inventory, or missing image)` ) ); } const marketplaceBox = root.createComponent(Box, { padding: 'base' }); marketplaceBox.appendChild( root.createComponent(ChoiceList, { title: 'Select Marketplaces', choices: [ { label: 'Amazon', id: 'amazon' }, { label: 'eBay', id: 'ebay' }, { label: 'Google Shopping', id: 'google' }, ], value: selectedMarketplaces, onChange: (val: string[]) => { selectedMarketplaces = val; updateUI(); }, }) ); stack.appendChild(marketplaceBox); stack.appendChild(root.createComponent(Divider, {})); const checkboxBox = root.createComponent(Box, { padding: 'base' }); checkboxBox.appendChild( root.createComponent( Checkbox, { checked: includeVariants, onChange: (val: boolean) => { includeVariants = val; } }, 'Include all product variants' ) ); stack.appendChild(checkboxBox); content.appendChild(stack); }; const handlePublish = async () => { publishing = true; updateUI(); const eligibleProducts = products.filter(p => p.status === 'ACTIVE' && p.totalInventory > 0 && p.featuredImage ); if (eligibleProducts.length === 0) { error = 'No eligible products to publish'; publishing = false; updateUI(); return; } await new Promise(resolve => setTimeout(resolve, 1500)); success = true; publishing = false; updateUI(); api.close(); }; // Fetch product details using direct API const productIds = api.data.selected.map(item => item.id); const response = await api.query(` query GetProducts($ids: [ID!]!) { nodes(ids: $ids) { ... on Product { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } featuredImage { url } } } } `, { variables: { ids: productIds } }); products = response.data.nodes.filter((p: any) => p !== null); secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Publish to Marketplace', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
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.
Anchor to Product purchase option action ,[object Object]Product purchase option action target
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.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add an action extension that syncs product purchase option settings (subscriptions, pre-orders, try-before-you-buy) with an external inventory management system. This example demonstrates calling your app backend to update fulfillment rules and stock allocation based on the purchase option type.
React
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> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-purchase-option.action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let syncOptions = ['inventory']; let overwriteExisting = false; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const updateUI = () => { content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Purchase option synced successfully!') ); } if (error) { stack.appendChild( root.createComponent(Banner, { tone: 'critical' }, error) ); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { name: 'syncOptions', title: 'Select data to sync', value: syncOptions, onChange: (value: string[]) => { syncOptions = value; updatePrimaryButton(); }, choices: [ { label: 'Inventory allocation rules', value: 'inventory' }, { label: 'Fulfillment settings', value: 'fulfillment' }, { label: 'Pricing tiers', value: 'pricing' }, ], }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent( Checkbox, { checked: overwriteExisting, onChange: (value: boolean) => { overwriteExisting = value; }, }, 'Overwrite existing settings in inventory system' ) ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); }; const updatePrimaryButton = () => { primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent( Button, { onPress: handleSync, disabled: loading || success || syncOptions.length === 0, }, loading ? 'Syncing...' : 'Sync Now' ) ); }; const handleSync = async () => { loading = true; error = null; updateUI(); updatePrimaryButton(); const purchaseOptionId = api.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'); } success = true; updateUI(); updatePrimaryButton(); api.close(); } catch (err) { error = err instanceof Error ? err.message : 'Failed to sync with inventory system'; updateUI(); } finally { loading = false; updatePrimaryButton(); } }; updatePrimaryButton(); secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Sync with Inventory System', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that fetches and displays subscription analytics for a product's purchase options using the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access). This example demonstrates how to query the [GraphQL Admin API](/docs/api/admin-graphql) to retrieve selling plan group details and subscription contract counts.
React
import React from 'react'; import { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Divider, Checkbox, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.product-purchase-option.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [metrics, setMetrics] = useState(null); const [includeInactive, setIncludeInactive] = useState(false); useEffect(() => { fetchMetrics(); }, []); const fetchMetrics = async () => { setLoading(true); setError(null); try { const productId = data.selected[0].id; const result = await query(` query GetProductSubscriptionMetrics($productId: ID!) { product(id: $productId) { title sellingPlanGroups(first: 10) { edges { node { id name sellingPlans(first: 5) { edges { node { id name billingPolicy { ... on SellingPlanRecurringBillingPolicy { interval intervalCount } } } } } } } } } } `, { variables: { productId } }); if (result.data?.product) { const product = result.data.product; const groups = product.sellingPlanGroups.edges.map(e => e.node); setMetrics({ productTitle: product.title, totalGroups: groups.length, groups: groups.map(g => ({ name: g.name, planCount: g.sellingPlans.edges.length, plans: g.sellingPlans.edges.map(p => p.node.name) })) }); } } catch (err) { setError('Failed to fetch subscription metrics'); } finally { setLoading(false); } }; return ( <AdminAction title="Subscription Metrics" primaryAction={ <Button onPress={fetchMetrics} disabled={loading}> {loading ? 'Loading...' : 'Refresh'} </Button> } secondaryAction={<Button onPress={close}>Close</Button>} > <BlockStack gap="base"> {error && <Banner tone="critical">{error}</Banner>} {metrics && ( <> <Banner tone="info"> {metrics.productTitle}: {metrics.totalGroups} selling plan group(s) </Banner> <Divider /> <Checkbox checked={includeInactive} onChange={setIncludeInactive} > Include inactive plans in analysis </Checkbox> <Box padding="base"> <BlockStack gap="tight"> {metrics.groups.map((group, idx) => ( <Banner key={idx} tone="success"> {group.name}: {group.planCount} plan(s) - {group.plans.join(', ')} </Banner> ))} {metrics.groups.length === 0 && ( <Banner tone="warning">No selling plan groups configured</Banner> )} </BlockStack> </Box> </> )} </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.product-purchase-option.action.render', (root, api) => { let loading = true; let error: string | null = null; let metrics: any = null; let includeInactive = false; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const fetchMetrics = async () => { loading = true; error = null; updateUI(); try { const productId = api.data.selected[0].id; const result = await api.query(` query GetProductSubscriptionMetrics($productId: ID!) { product(id: $productId) { title sellingPlanGroups(first: 10) { edges { node { id name sellingPlans(first: 5) { edges { node { id name billingPolicy { ... on SellingPlanRecurringBillingPolicy { interval intervalCount } } } } } } } } } } `, { variables: { productId } }); if (result.data?.product) { const product = result.data.product; const groups = product.sellingPlanGroups.edges.map((e: any) => e.node); metrics = { productTitle: product.title, totalGroups: groups.length, groups: groups.map((g: any) => ({ name: g.name, planCount: g.sellingPlans.edges.length, plans: g.sellingPlans.edges.map((p: any) => p.node.name) })) }; } } catch (err) { error = 'Failed to fetch subscription metrics'; } finally { loading = false; updateUI(); } }; const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); primaryAction.appendChild( root.createComponent(Button, { onPress: fetchMetrics, disabled: loading }, loading ? 'Loading...' : 'Refresh' ) ); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } if (metrics) { stack.appendChild( root.createComponent(Banner, { tone: 'info' }, `${metrics.productTitle}: ${metrics.totalGroups} selling plan group(s)` ) ); stack.appendChild(root.createComponent(Divider, {})); stack.appendChild( root.createComponent(Checkbox, { checked: includeInactive, onChange: (val: boolean) => { includeInactive = val; updateUI(); } }, 'Include inactive plans in analysis') ); const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'tight' }); metrics.groups.forEach((group: any) => { innerStack.appendChild( root.createComponent(Banner, { tone: 'success' }, `${group.name}: ${group.planCount} plan(s) - ${group.plans.join(', ')}` ) ); }); if (metrics.groups.length === 0) { innerStack.appendChild( root.createComponent(Banner, { tone: 'warning' }, 'No selling plan groups configured') ); } box.appendChild(innerStack); stack.appendChild(box); } content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Close') ); fetchMetrics(); const adminAction = root.createComponent(AdminAction, { title: 'Subscription Metrics', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
Anchor to Best practicesBest practices
- 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.
Anchor to LimitationsLimitations
- 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.rendertarget only appears when the product has a selling plan group associated with it. - Configuration target availability: The
admin.product-details.configuration.rendertarget 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
nullfrom a block extension collapses the block rather than removing it from the page. Blocks can't be fully hidden at runtime.