Guide for building Shopify POS (Point of Sale) UI extensions that integrate custom functionality into Shopify's retail interface. Use this skill when the user needs to create POS tiles, modals, blocks, or actions for the smart grid, cart, customer details, order details, product details, or post-purchase screens. Covers targets, components, APIs, and development workflow for POS app extensions.
Build custom extensions that integrate directly into Shopify's Point of Sale interface on iOS and Android devices.
Enable POS embedding: In Partner Dashboard > App > Configuration, set "Embed app in Shopify POS" to True.
POS UI extensions have three interconnected parts:
shopify app generate extension --template pos_ui --name "my-pos-extension"
api_version = "2025-10"
[[extensions]]
type = "ui_extension"
name = "my-pos-extension"
handle = "my-pos-extension"
[[extensions.targeting]]
module = "./src/Tile.tsx"
target = "pos.home.tile.render"
[[extensions.targeting]]
module = "./src/Modal.tsx"
target = "pos.home.modal.render"
See references/targets.md for all available targets.
| Type | Purpose | Example |
|---|---|---|
| Tile | Smart grid button on home screen | pos.home.tile.render |
| Modal | Full-screen interface | pos.home.modal.render |
| Block | Inline content section | pos.product-details.block.render |
| Menu Item | Action menu button | pos.customer-details.action.menu-item.render |
Home Screen (Smart Grid)
// Tile.tsx - Entry point on POS home
import { Tile, reactExtension } from '@shopify/ui-extensions-react/point-of-sale';
export default reactExtension('pos.home.tile.render', () => <TileComponent />);
function TileComponent() {
return <Tile title="My App" subtitle="Tap to open" enabled={true} />;
}
Modal (Full Screen)
// Modal.tsx - Launches when tile is tapped
import { Screen, Navigator, Text, Button, useApi, reactExtension } from '@shopify/ui-extensions-react/point-of-sale';
export default reactExtension('pos.home.modal.render', () => <ModalComponent />);
function ModalComponent() {
const api = useApi<'pos.home.modal.render'>();
return (
<Navigator>
<Screen name="Main" title="My Extension">
<Text>Welcome to my POS extension</Text>
<Button title="Close" onPress={() => api.navigation.dismiss()} />
</Screen>
</Navigator>
);
}
Block (Inline Content)
// ProductBlock.tsx
import { Section, Text, reactExtension, useApi } from '@shopify/ui-extensions-react/point-of-sale';
export default reactExtension('pos.product-details.block.render', () => <ProductBlock />);
function ProductBlock() {
const { product } = useApi<'pos.product-details.block.render'>();
const productData = product.getProduct();
return (
<Section title="Custom Info">
<Text>Product ID: {productData?.id}</Text>
</Section>
);
}
See references/components.md for all available components.
Layout & Structure
Screen - Navigation screen with title, loading state, actionsNavigator - Screen navigation containerScrollView - Scrollable content containerSection - Card-like grouping containerStack - Horizontal/vertical layoutList - Structured data rowsActions
Button - Tappable action buttonTile - Smart grid tile (home screen only)Selectable - Make components tappableForms
TextField, TextArea - Text inputNumberField - Numeric inputEmailField - Email with validationDateField, DatePicker - Date selectionRadioButtonList - Single selectionStepper - Increment/decrement controlPinPad - Secure PIN entryFeedback
Banner - Important messagesDialog - Confirmation promptsBadge - Status indicatorsMedia
Icon - POS icon catalogImage - Visual contentCameraScanner - Barcode/QR scanningSee references/apis.md for all available APIs.
import { useApi } from '@shopify/ui-extensions-react/point-of-sale';
function MyComponent() {
const api = useApi<'pos.home.modal.render'>();
// Access various APIs based on target
const { cart, customer, session, navigation, toast } = api;
}
Cart API - Modify cart contents
const { cart } = useApi<'pos.home.modal.render'>();
// Add item
await cart.addLineItem({ variantId: 'gid://shopify/ProductVariant/123', quantity: 1 });
// Apply discount
await cart.applyCartDiscount({ type: 'percentage', value: 10, title: '10% Off' });
// Get cart
const currentCart = cart.getCart();
Session API - Authentication and session data
const { session } = useApi<'pos.home.modal.render'>();
// Get session token for backend auth
const token = await session.getSessionToken();
// Get current staff member
const staff = session.currentSession;
Customer API - Customer data access
const { customer } = useApi<'pos.customer-details.block.render'>();
const customerData = customer.getCustomer();
Toast API - Show notifications
const { toast } = useApi<'pos.home.modal.render'>();
toast.show('Item added successfully');
Navigation API - Screen navigation
const { navigation } = useApi<'pos.home.modal.render'>();
navigation.dismiss(); // Close modal
navigation.navigate('ScreenName'); // Navigate to screen
Scanner API - Barcode scanning
const { scanner } = useApi<'pos.home.modal.render'>();
const result = await scanner.scanBarcode();
Print API - Receipt printing
const { print } = useApi<'pos.home.modal.render'>();
await print.printDocument(documentContent);
Available for extensions targeting 2025-07 or later (requires POS 10.6.0+).
const response = await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query GetProduct($id: ID!) {
product(id: $id) {
title
variants(first: 10) {
nodes { id title inventoryQuantity }
}
}
}
`,
variables: { id: 'gid://shopify/Product/123' }
})
});
Declare required scopes in shopify.app.toml:
[access_scopes]
scopes = "read_products,write_products,read_customers"
shopify app dev
Open the Shopify POS app on your device and connect to the development store.
shopify app deploy
const { storage } = useApi<'pos.home.modal.render'>();
// Store data
await storage.setItem('key', JSON.stringify(data));
// Retrieve data
const stored = await storage.getItem('key');
const data = stored ? JSON.parse(stored) : null;
// Tile.tsx
import { Tile, reactExtension } from '@shopify/ui-extensions-react/point-of-sale';
export default reactExtension('pos.home.tile.render', () => (
<Tile title="Loyalty Points" subtitle="Check & redeem" enabled={true} />
));
// Modal.tsx
import {
Screen, Navigator, Text, Button, Section, Stack,
useApi, reactExtension
} from '@shopify/ui-extensions-react/point-of-sale';
import { useState, useEffect } from 'react';
export default reactExtension('pos.home.modal.render', () => <LoyaltyModal />);
function LoyaltyModal() {
const { cart, session, navigation, toast } = useApi<'pos.home.modal.render'>();
const [points, setPoints] = useState(0);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchPoints();
}, []);
async function fetchPoints() {
const token = await session.getSessionToken();
const currentCart = cart.getCart();
const customerId = currentCart?.customer?.id;
if (!customerId) {
setLoading(false);
return;
}
const res = await fetch('https://your-backend.com/api/points', {
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ customerId })
});
const data = await res.json();
setPoints(data.points);
setLoading(false);
}
async function redeemPoints() {
await cart.applyCartDiscount({
type: 'fixedAmount',
value: points / 100,
title: 'Loyalty Redemption'
});
toast.show('Points redeemed!');
navigation.dismiss();
}
return (
<Navigator>
<Screen name="Main" title="Loyalty Points" isLoading={loading}>
<Section title="Current Balance">
<Stack direction="vertical" spacing={2}>
<Text variant="headingLarge">{points} points</Text>
<Text>Worth ${(points / 100).toFixed(2)}</Text>
</Stack>
</Section>
<Button
title="Redeem All Points"
type="primary"
onPress={redeemPoints}
disabled={points === 0}
/>
<Button title="Close" onPress={() => navigation.dismiss()} />
</Screen>
</Navigator>
);
}