Skip to main content

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 TypeWhere It AppearsUse Case
Admin ActionAction menus on resource pagesTrigger workflows from context menus
Admin BlockEmbedded on resource detail pagesDisplay app data alongside Shopify data
Product ConfigurationProduct creation/editing formsAdd custom fields to product setup
NavigationAdmin sidebarDeep-link to app sections
Bulk ActionResource list pagesOperate 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"
Extension Targets

The target field determines where your extension appears. Common targets include:

  • admin.product-details.action.render -- Product detail page actions
  • admin.order-details.action.render -- Order detail page actions
  • admin.customer-details.action.render -- Customer detail page actions
  • admin.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>
);
}
Extension Sandboxing

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
Development Workflow

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:

  1. Test on multiple screen sizes -- The Admin is used on desktops, tablets, and mobile devices
  2. Handle loading states -- Always show a spinner or skeleton while fetching data
  3. Handle error states -- Network failures happen; show a helpful message and retry option
  4. Respect rate limits -- The Admin API has rate limits; cache responses where possible
  5. Localize strings -- Use the locales/ directory for all user-facing text
  6. Check accessibility -- Ensure your extension is keyboard-navigable and screen-reader friendly
Extension Size Limits

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.

Next Steps

Continue to Checkout Extensions to learn how to customize the Shopify checkout experience with custom UI, payment logic, and shipping rules.