Skip to main content
Migrate to Polaris

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

AdminAction

The AdminAction component configures the modal that appears when users trigger your admin action extension. Use AdminAction to set the title, primary button, secondary button, and loading state for the modal.

Learn how to build an admin action extension.

Support
Targets (46)

Supported targets


Props for the AdminAction component, used by Admin Action extensions to configure the title, primary and secondary action buttons, and loading state of the action modal.

Anchor to loading
loading
boolean
Default: false

Whether the action modal is in a loading state. Set this to true while fetching data or processing a request to display a loading indicator in place of the modal's content.

Anchor to primaryAction
primaryAction
RemoteFragment

The primary action button in the modal's footer, rendered as a Button. Use this for the main action the user can take, such as "Save" or "Submit".

Anchor to secondaryAction
secondaryAction
RemoteFragment

The secondary action button in the modal's footer, rendered as a Button. Use this for an alternative action, such as "Cancel" or "Discard".

Anchor to title
title
string

The title displayed at the top of the action modal. If not provided, then the extension's name is used instead. Titles longer than 40 characters will be truncated.


Anchor to Configure action modal with buttonsConfigure action modal with buttons

Sync product data to a warehouse system from a modal with primary and cancel actions. This example uses AdminAction with primaryAction and secondaryAction Button props to confirm or dismiss the sync.

Configure action modal with buttons

Sync product data to a warehouse system from a modal with primary and cancel actions. This example uses `AdminAction` with `primaryAction` and `secondaryAction` [Button](/docs/api/admin-extensions/2025-07/ui-components/actions/button) props to confirm or dismiss the sync.

Configure action modal with buttons

import {reactExtension, useApi, AdminAction, Button, Text, BlockStack} from '@shopify/ui-extensions-react/admin';

function App() {
const {data, close} = useApi('admin.product-details.action.render');
const productId = data.selected[0]?.id;

return (
<AdminAction
title="Sync to warehouse"
primaryAction={
<Button
onPress={async () => {
await fetch('/api/products/sync', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({productId}),
});
close();
}}
>
Sync product
</Button>
}
secondaryAction={
<Button onPress={() => close()}>Cancel</Button>
}
>
<BlockStack gap>
<Text>
Sync product {productId} to your warehouse management system. This
will update inventory counts, pricing, and metadata.
</Text>
</BlockStack>
</AdminAction>
);
}

export default reactExtension(
'admin.product-details.action.render',
() => <App />,
);
import {extension, AdminAction, Button, Text, BlockStack} from '@shopify/ui-extensions/admin';

export default extension(
'admin.product-details.action.render',
(root, api) => {
const {data, close} = api;
const productId = data.selected[0]?.id;

const content = root.createComponent(BlockStack, {gap: true});
const message = root.createComponent(
Text,
{},
`Sync product ${productId} to your warehouse management system. This will update inventory counts, pricing, and metadata.`,
);
content.appendChild(message);

const primaryAction = root.createComponent(
Button,
{
onPress: async () => {
await fetch('/api/products/sync', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({productId}),
});
close();
},
},
'Sync product',
);

const secondaryAction = root.createComponent(
Button,
{onPress: () => close()},
'Cancel',
);

const action = root.createComponent(AdminAction, {
title: 'Sync to warehouse',
primaryAction,
secondaryAction,
});

action.appendChild(content);
root.appendChild(action);
},
);

Build a form inside an action modal with TextField and Select inputs. This example collects a warehouse SKU and location assignment, submitting the form data through the primary action button.

Build a modal form

import React from 'react';
import {reactExtension, useApi, AdminAction, Button, TextField, Select, BlockStack} from '@shopify/ui-extensions-react/admin';

function App() {
const {data, close} = useApi('admin.product-details.action.render');
const productId = data.selected[0]?.id;

return (
<AdminAction
title="Assign warehouse location"
primaryAction={
<Button
onPress={async () => {
await fetch('/api/products/assign-warehouse', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({productId}),
});
close();
}}
>
Assign to warehouse
</Button>
}
secondaryAction={
<Button onPress={() => close()}>Cancel</Button>
}
>
<BlockStack gap>
<TextField label="Warehouse SKU" name="warehouseSku" required />
<Select
label="Target warehouse"
name="warehouse"
options={[
{label: 'East Coast — New York', value: 'nyc'},
{label: 'West Coast — Los Angeles', value: 'lax'},
{label: 'Central — Chicago', value: 'chi'},
]}
/>
</BlockStack>
</AdminAction>
);
}

export default reactExtension(
'admin.product-details.action.render',
() => <App />,
);
import {extension, AdminAction, Button, TextField, Select, BlockStack} from '@shopify/ui-extensions/admin';

export default extension(
'admin.product-details.action.render',
(root, api) => {
const {data, close} = api;
const productId = data.selected[0]?.id;

const content = root.createComponent(BlockStack, {gap: true});

const skuField = root.createComponent(TextField, {
label: 'Warehouse SKU',
name: 'warehouseSku',
required: true,
});

const warehouseSelect = root.createComponent(Select, {
label: 'Target warehouse',
name: 'warehouse',
options: [
{label: 'East Coast — New York', value: 'nyc'},
{label: 'West Coast — Los Angeles', value: 'lax'},
{label: 'Central — Chicago', value: 'chi'},
],
});

content.appendChild(skuField);
content.appendChild(warehouseSelect);

const primaryAction = root.createComponent(
Button,
{
onPress: async () => {
await fetch('/api/products/assign-warehouse', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({productId}),
});
close();
},
},
'Assign to warehouse',
);

const secondaryAction = root.createComponent(
Button,
{onPress: () => close()},
'Cancel',
);

const action = root.createComponent(AdminAction, {
title: 'Assign warehouse location',
primaryAction,
secondaryAction,
});

action.appendChild(content);
root.appendChild(action);
},
);

Anchor to Load data into action modalLoad data into action modal

Show a ProgressIndicator while fetching data from the GraphQL Admin API, then replace it with product details. This example queries product information when the modal opens and displays the results after they've loaded.

Load data into action modal

import React, {useState, useEffect} from 'react';
import {reactExtension, useApi, AdminAction, Button, Text, ProgressIndicator, BlockStack} from '@shopify/ui-extensions-react/admin';

function App() {
const {data, close, query} = useApi('admin.product-details.action.render');
const productId = data.selected[0]?.id;
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
query(
`query Product($id: ID!) {
product(id: $id) { title status totalInventory }
}`,
{variables: {id: productId}},
).then((result) => {
setProduct(result?.data?.product);
setLoading(false);
});
}, [productId, query]);

return (
<AdminAction
title="Product details"
primaryAction={<Button onPress={() => close()}>Done</Button>}
>
<BlockStack gap>
{loading ? (
<ProgressIndicator
size="small-200"
accessibilityLabel="Loading product details"
/>
) : product ? (
<>
<Text fontWeight="bold">{product.title}</Text>
<Text>Status: {product.status}</Text>
<Text>Inventory: {product.totalInventory} units</Text>
</>
) : null}
</BlockStack>
</AdminAction>
);
}

export default reactExtension(
'admin.product-details.action.render',
() => <App />,
);
import {extension, AdminAction, Button, Text, ProgressIndicator, BlockStack} from '@shopify/ui-extensions/admin';

export default extension(
'admin.product-details.action.render',
async (root, api) => {
const {data, close, query} = api;
const productId = data.selected[0]?.id;

const content = root.createComponent(BlockStack, {gap: true});

const loader = root.createComponent(ProgressIndicator, {
size: 'small-200',
accessibilityLabel: 'Loading product details',
});
content.appendChild(loader);

const primaryAction = root.createComponent(
Button,
{onPress: () => close()},
'Done',
);

const action = root.createComponent(AdminAction, {
title: 'Product details',
primaryAction,
});
action.appendChild(content);
root.appendChild(action);

const result = await query(
`query Product($id: ID!) {
product(id: $id) { title status totalInventory }
}`,
{variables: {id: productId}},
);

content.removeChild(loader);

const product = result?.data?.product;
if (product) {
const title = root.createComponent(Text, {fontWeight: 'bold'}, product.title);
const status = root.createComponent(Text, {}, `Status: ${product.status}`);
const inventory = root.createComponent(Text, {}, `Inventory: ${product.totalInventory} units`);
content.appendChild(title);
content.appendChild(status);
content.appendChild(inventory);
}
},
);

  • Keep the modal focused on a single task: Each action extension should handle one specific workflow so merchants can complete it quickly without confusion.
  • Show a loading state while fetching initial data: Use the loading state to prevent merchants from interacting with incomplete content while your extension initializes.
  • Place the most important action as the primary action: The primary action should be the main submit or confirm action. Use the secondary action for cancel or dismiss.

  • The Shopify admin controls the modal dimensions. Extensions can't adjust the width or height.
  • The Shopify admin renders the modal as a blocking overlay. The underlying page isn't interactive until the merchant completes or dismisses the action.

Was this page helpful?