Admin UI Extensions
Admin UI extensions let you embed custom interfaces directly into the Shopify Admin. Instead of forcing merchants to navigate to your app's full page, you can surface functionality exactly where merchants need it -- on product pages, order detail views, customer profiles, and more. These extensions render inside the Shopify Admin using a sandboxed environment with access to Shopify's UI components.
Extension Types Overview
Shopify provides several categories of Admin UI extensions:
| Extension Type | Where It Appears | Use Case |
|---|---|---|
| Admin Action | Action menus on resource pages | Trigger workflows from context menus |
| Admin Block | Embedded on resource detail pages | Display app data alongside Shopify data |
| Product Configuration | Product creation/editing forms | Add custom fields to product setup |
| Navigation | Admin sidebar | Deep-link to app sections |
| Bulk Action | Resource list pages | Operate on multiple items at once |
Admin Action Extensions
Admin actions appear in the "More actions" dropdown on resource pages. They open a modal overlay where your extension renders.
Creating an Admin Action
# Generate an admin action extension
shopify app generate extension --template admin_action --name ai-description-generator
This creates the extension scaffold:
extensions/ai-description-generator/
├── src/
│ └── ActionExtension.tsx
├── locales/
│ └── en.default.json
├── shopify.extension.toml
└── package.json
Extension Configuration
# extensions/ai-description-generator/shopify.extension.toml
api_version = "2025-01"
[[extensions]]
type = "admin_action"
name = "Generate AI Description"
handle = "ai-description-generator"
[[extensions.targeting]]
module = "./src/ActionExtension.tsx"
target = "admin.product-details.action.render"
The target field determines where your extension appears. Common targets include:
admin.product-details.action.render-- Product detail page actionsadmin.order-details.action.render-- Order detail page actionsadmin.customer-details.action.render-- Customer detail page actionsadmin.product-index.action.render-- Product list page actions
Building the Action UI
Admin extensions use Shopify's remote UI components -- a subset of Polaris components designed for the sandboxed extension environment:
// extensions/ai-description-generator/src/ActionExtension.tsx
import { useEffect, useState } from 'react';
import {
reactExtension,
useApi,
AdminAction,
BlockStack,
Button,
InlineStack,
ProgressIndicator,
Select,
Text,
TextField,
} from '@shopify/ui-extensions-react/admin';
const TARGET = 'admin.product-details.action.render';
export default reactExtension(TARGET, () => <GenerateDescriptionAction />);
function GenerateDescriptionAction() {
const { data, close, intents } = useApi(TARGET);
const [tone, setTone] = useState('professional');
const [generatedDescription, setGeneratedDescription] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const productId = data.selected?.[0]?.id;
async function handleGenerate() {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/generate-description', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId,
tone,
}),
});
if (!response.ok) {
throw new Error('Failed to generate description');
}
const result = await response.json();
setGeneratedDescription(result.description);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
}
async function handleApply() {
try {
// Use the Admin API directly from the extension
const response = await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
mutation updateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
descriptionHtml
}
userErrors {
field
message
}
}
}
`,
variables: {
input: {
id: productId,
descriptionHtml: generatedDescription,
},
},
}),
});
close();
} catch (err) {
setError('Failed to update product');
}
}
return (
<AdminAction
title="Generate AI Description"
primaryAction={
<Button
onPress={handleApply}
disabled={!generatedDescription}
>
Apply Description
</Button>
}
secondaryAction={
<Button onPress={() => close()}>Cancel</Button>
}
>
<BlockStack gap="large">
<Select
label="Tone"
value={tone}
onChange={setTone}
options={[
{ label: 'Professional', value: 'professional' },
{ label: 'Casual & Friendly', value: 'casual' },
{ label: 'Luxurious', value: 'luxurious' },
{ label: 'Technical', value: 'technical' },
{ label: 'Playful', value: 'playful' },
]}
/>
<Button onPress={handleGenerate} disabled={loading}>
{loading ? 'Generating...' : 'Generate Description'}
</Button>
{loading && <ProgressIndicator size="small" />}
{error && (
<Text tone="critical">{error}</Text>
)}
{generatedDescription && (
<BlockStack gap="small">
<Text variant="headingSm">Generated Description</Text>
<TextField
label="Description"
value={generatedDescription}
onChange={setGeneratedDescription}
multiline={6}
autoComplete="off"
/>
</BlockStack>
)}
</BlockStack>
</AdminAction>
);
}
Admin extensions run in a sandboxed environment. You cannot access window, document, or use arbitrary npm packages. Only Shopify's provided UI components and APIs are available. If you need to call your backend, use fetch() with relative URLs -- Shopify proxies requests to your app server.
Admin Block Extensions
Admin blocks are persistent UI panels that appear on resource detail pages. Unlike actions (which open as modals), blocks are always visible and update in real time.
// extensions/loyalty-block/src/BlockExtension.tsx
import { useEffect, useState } from 'react';
import {
reactExtension,
useApi,
AdminBlock,
BlockStack,
InlineStack,
Text,
Badge,
Divider,
ProgressIndicator,
Heading,
} from '@shopify/ui-extensions-react/admin';
const TARGET = 'admin.customer-details.block.render';
export default reactExtension(TARGET, () => <LoyaltyBlock />);
function LoyaltyBlock() {
const { data } = useApi(TARGET);
const [loyaltyData, setLoyaltyData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const customerId = data.selected?.[0]?.id;
useEffect(() => {
async function fetchLoyaltyData() {
try {
const response = await fetch(
`/api/loyalty/customer?id=${encodeURIComponent(customerId)}`
);
const result = await response.json();
setLoyaltyData(result);
} catch (err) {
console.error('Failed to fetch loyalty data:', err);
} finally {
setLoading(false);
}
}
if (customerId) {
fetchLoyaltyData();
}
}, [customerId]);
if (loading) {
return (
<AdminBlock title="Loyalty Program">
<ProgressIndicator size="small" />
</AdminBlock>
);
}
if (!loyaltyData) {
return (
<AdminBlock title="Loyalty Program">
<Text>Customer is not enrolled in the loyalty program.</Text>
</AdminBlock>
);
}
return (
<AdminBlock title="Loyalty Program">
<BlockStack gap="base">
<InlineStack gap="base" blockAlignment="center">
<Text variant="headingSm">Tier:</Text>
<Badge tone={loyaltyData.tier === 'Gold' ? 'warning' : 'info'}>
{loyaltyData.tier}
</Badge>
</InlineStack>
<Divider />
<InlineStack gap="large">
<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">Points Balance</Text>
<Heading>{loyaltyData.pointsBalance.toLocaleString()}</Heading>
</BlockStack>
<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">Lifetime Points</Text>
<Heading>{loyaltyData.lifetimePoints.toLocaleString()}</Heading>
</BlockStack>
<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">Redemptions</Text>
<Heading>{loyaltyData.totalRedemptions}</Heading>
</BlockStack>
</InlineStack>
<Divider />
<Text variant="bodySm" tone="subdued">
Member since {new Date(loyaltyData.enrolledAt).toLocaleDateString()}
</Text>
</BlockStack>
</AdminBlock>
);
}
Product Configuration Extension
Product configuration extensions add custom fields to the product creation and editing experience. These are ideal for apps that need additional product metadata.
// extensions/product-ai-config/src/ProductConfig.tsx
import { useState } from 'react';
import {
reactExtension,
useApi,
AdminBlock,
BlockStack,
Checkbox,
Select,
Text,
TextField,
} from '@shopify/ui-extensions-react/admin';
const TARGET = 'admin.product-details.configuration.render';
export default reactExtension(TARGET, () => <ProductAIConfig />);
function ProductAIConfig() {
const { data, applyMetafieldsChange } = useApi(TARGET);
const [autoDescription, setAutoDescription] = useState(false);
const [targetAudience, setTargetAudience] = useState('general');
const [keywords, setKeywords] = useState('');
async function handleAutoDescriptionToggle(checked: boolean) {
setAutoDescription(checked);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'auto_description',
value: String(checked),
valueType: 'boolean',
});
}
async function handleAudienceChange(value: string) {
setTargetAudience(value);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'target_audience',
value,
valueType: 'single_line_text_field',
});
}
async function handleKeywordsChange(value: string) {
setKeywords(value);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'seo_keywords',
value,
valueType: 'single_line_text_field',
});
}
return (
<AdminBlock title="AI Configuration">
<BlockStack gap="base">
<Text variant="bodySm" tone="subdued">
Configure how AI tools interact with this product.
</Text>
<Checkbox
label="Auto-generate description on save"
checked={autoDescription}
onChange={handleAutoDescriptionToggle}
/>
<Select
label="Target Audience"
value={targetAudience}
onChange={handleAudienceChange}
options={[
{ label: 'General', value: 'general' },
{ label: 'Young Adults (18-25)', value: 'young_adults' },
{ label: 'Professionals', value: 'professionals' },
{ label: 'Parents', value: 'parents' },
{ label: 'Enthusiasts', value: 'enthusiasts' },
]}
/>
<TextField
label="SEO Keywords"
value={keywords}
onChange={handleKeywordsChange}
helpText="Comma-separated keywords to emphasize in AI-generated content"
autoComplete="off"
/>
</BlockStack>
</AdminBlock>
);
}
Deploying Extensions
Extensions are deployed alongside your app using the Shopify CLI:
# Development mode -- live reload
shopify app dev
# Deploy to production
shopify app deploy
# Deploy only extensions (skip app code)
shopify app deploy --reset
Extension Versioning
Each deployment creates a new version of your extensions. Shopify manages rollouts:
# Check extension status
shopify app versions list
# Release a specific version
shopify app release --version 1.2.0
During development, shopify app dev starts a tunnel and enables hot reloading for your extensions. Changes to extension code are reflected immediately in the Shopify Admin without redeploying. Use a development store (not a live store) for testing.
Pre-Deployment Checklist
Before deploying Admin UI extensions to production:
- Test on multiple screen sizes -- The Admin is used on desktops, tablets, and mobile devices
- Handle loading states -- Always show a spinner or skeleton while fetching data
- Handle error states -- Network failures happen; show a helpful message and retry option
- Respect rate limits -- The Admin API has rate limits; cache responses where possible
- Localize strings -- Use the
locales/directory for all user-facing text - Check accessibility -- Ensure your extension is keyboard-navigable and screen-reader friendly
Admin UI extensions have a maximum bundle size of 512 KB (compressed). Keep dependencies minimal. Do not bundle large libraries -- use Shopify's provided UI components instead of bringing your own component library.
Continue to Checkout Extensions to learn how to customize the Shopify checkout experience with custom UI, payment logic, and shipping rules.