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 upgrade guide to upgrade your extension.

POS UI extensions

Build extensions that integrate into Shopify's Point of Sale interface. For example, you can add a clickable tile to the POS home screen that launches your app's features, insert custom content sections into checkout and sales processes, or show additional product details.

Extensions run in the context of key merchant workflows, so always prioritize performance.

POS UI extensions require a set of essential files, including a TOML configuration file and JavaScript or TypeScript modules containing your extension code.

Use Shopify CLI to scaffold your extension with the essential configuration and files. You can alter the default configuration later to customize the way your POS UI extension operates.

Generate scaffold

cd my-app
shopify app generate extension

Anchor to Building your extensionBuilding your extension

POS UI extensions are made up of three interconnected parts: targets that determine where your extension appears in the POS interface, target APIs that provide access to data and functionality based on your chosen target, and UI components that define which interface elements you can use.

Anchor to Targets: Choose where your extension appearsTargets: Choose where your extension appears

Targets define where your extensions appear within Shopify's POS interface and what capabilities they have. There are four types of targets:

Target typeDescription
TileAppear on the smart grid (homepage) of the POS system. Use to show status, quickly access common actions, and initiate workflows.
ActionTrigger workflows and display modals. This target has two varieties, commonly used together:

  • Menu item: Add buttons to action menus on native POS screens. Use to provide entry points that launch modals.
  • Modal: Display full-screen interfaces for complete workflows, forms, or multi-step processes. Modals are launched from tiles or menu items.
BlockAppear as inline content within native POS screens. Use to render content such as metrics, instructions, or printed content on receipts.
EventObserve transaction events in the background without displaying UI. Use to respond to merchant actions, sync data, or collect analytics.
Placeholder image

Anchor to Target APIs: Define what your extension doesTarget APIs: Define what your extension does

Your extension might show customer loyalty points during checkout, add a custom "Print warranty card" action after completing a purchase, or capture additional product details when staff view inventory.

Use Target APIs to access the data and functionality you need for each scenario—Customer API for loyalty points, Session API for transaction details, or Storage API for capturing custom information.

Storage API: Getting a single value

import React from 'react';
import {
Tile,
reactExtension,
useApi,
} from '@shopify/ui-extensions-react/point-of-sale';

const TileComponent = () => {
const api = useApi<'pos.home.tile.render'>();
return (
<Tile
title="Storage app"
subtitle="Get example"
onPress={async () => {
const storedData = await api.storage.get('key');
api.toast.show(String(storedData ?? ''));
}}
enabled
/>
);
};

export default reactExtension('pos.home.tile.render', () => <TileComponent />);
import {Tile, extension} from '@shopify/ui-extensions/point-of-sale';

export default extension('pos.home.tile.render', (root, api) => {
const tile = root.createComponent(Tile, {
title: 'Storage app',
subtitle: 'Get example',
enabled: `true`,
onPress: async () => {
const storedData = await api.storage.get('key');
api.toast.show(String(storedData ?? ''));
},
});

root.append(tile);
});

Anchor to UI components: Design your interfaceUI components: Design your interface

These components are built with remote-ui. For the equivalents used in newer API versions, see web components.

UI components are the building blocks that you use to display data and trigger API functions in POS UI extensions. They render optimized interfaces for retail workflows in the POS app on iOS and Android devices.

Components are available as React components or JavaScript objects, imported from @shopify/ui-extensions/point-of-sale.

Use UI components to build interfaces that automatically adapt across iOS and Android devices running Shopify POS.

Tile component: Render a tile on the smart grid

import React from 'react'
import { Tile, reactExtension, useApi } from '@shopify/ui-extensions-react/point-of-sale'

const TileComponent = () => {
const api = useApi()
return (
<Tile
title="My app"
subtitle="Hello world!"
onPress={() => {
api.action.presentModal()
}}
enabled
/>
)
}

export default reactExtension('pos.home.tile.render', () => {
return <TileComponent />
})
import {
Tile,
extension,
} from '@shopify/ui-extensions/point-of-sale';

export default extension(
'pos.home.tile.render',
(root, api) => {
const tile = root.createComponent(Tile, {
title: 'My app',
subtitle: 'Hello world!',
enabled: `true`,
onPress: () => {
api.action.presentModal();
},
});

root.append(tile);
},
);
Placeholder image

POS UI extensions rely on a shopify.extension.toml file that contains the extension's configuration. This includes the extension name, type, API version, and targeting definitions.

The name value is the merchant-facing name of the extension, so consider this value carefully. Individual tile, action, and block labels come from your extension's UI, not from name. We recommend that the api_version reflects the latest supported API version.

POS UI extensions use the following configuration properties:

api_version required

The version of the API that's being used for the extension. If provided in the [[extensions]] array, then the specified API version is used instead of the root level api_version.

[[extensions]] required

The name of the array that contains all extensions listed in the TOML file. Contains the following properties:

  • type: required The extension type. For more information, refer to Configuration for each extension type.

  • name: required The merchant-facing name of the extension. After you generate an extension, you're prompted to provide a name for your extension. The name property is translatable if it starts with a t: and uses a key defined in your translation data. For example, you might have a t:name key that matches a translation key called name. Learn more about localization.

  • handle: required The unique internal identifier for the extension. After you create a draft version of the extension, or deploy an extension, you can't change the handle value.

  • uid: required The extension user identifier that must be unique within the app. An app-scoped identifier used by shopify app deploy to determine whether an extension is being created, updated, or deleted. This identifier is generated automatically when you scaffold your extension using Shopify CLI.

  • description: required The merchant-facing description of the extension. The description property is translatable if it starts with a t: and uses a key defined in your translation data. For example, t:description and you have a matching translation key called description. Learn more about localization.

[[extensions.targeting]] required

The name of the array that contains a target and its associated module. Contains the following properties:

  • target: required

    An identifier that specifies where you're injecting your extension into the POS interface.

  • module: required

    The path to the JavaScript or TypeScript file that contains your extension code. This file exports the extension function that renders your UI or handles events.

shopify.extension.toml

api_version = "2025-07"

[[extensions]]
type = "ui_extension"
name = "My POS UI extension"
handle = "my-pos-ui-extension"
uid = "3c9a17f4-6d2b-4e08-b1c5-7fa32e9d64b80af5c231"
description = "My POS UI extension description"

[[extensions.targeting]]
target = "pos.home.tile.render"
module = "./src/Tile.tsx"

[[extensions.targeting]]
target = "pos.home.modal.render"
module = "./src/Modal.tsx"

Use authenticated requests when your extension needs to fetch data or trigger actions on your own backend service. For example, you might need to display a customer's loyalty status, process a custom discount calculation, or log POS transactions to an external analytics system.

Extensions automatically include authorization headers when making requests to your app's domain. Relative URLs resolve against your application_url.

Note

App authentication is available as of POS version 10.6.0 for extensions targeting 2025-07 or later.

ID tokens are only returned for authenticated users with proper app permissions. Without correct permissions, the token will be null. POS Staff members aren't authenticated users. Learn more about configuring app permissions.

Make requests to your app's backend

import {
reactExtension,
useApi,
Text,
POSBlock,
POSBlockRow,
} from '@shopify/ui-extensions-react/point-of-sale';
import {useEffect, useState} from 'react';

const CustomerDetailsBlock = () => {
const {customer} = useApi<'pos.customer-details.block.render'>();
const [loyaltyInfo, setLoyaltyInfo] = useState<any>();
useEffect(() => {
getLoyaltyInfo();
}, [customer.id]);

async function getLoyaltyInfo() {
const res = await fetch(`/api/loyalty/${customer.id}`);
const json = await res.json();
setLoyaltyInfo(json.loyaltySummary);
}

return (
<POSBlock>
<POSBlockRow>
<Text>{loyaltyInfo}</Text>
</POSBlockRow>
</POSBlock>
);
};

export default reactExtension('pos.customer-details.block.render', () => (
<CustomerDetailsBlock />
));
import {
extension,
POSBlock,
Text,
POSBlockRow,
} from '@shopify/ui-extensions/point-of-sale';

export default extension('pos.customer-details.block.render', (root, api) => {
const customerId = api.customer.id;
const infoText = root.createComponent(Text);
const posBlockRow = root.createComponent(POSBlockRow);

async function getLoyaltyInfo() {
const res = await fetch(`/api/loyalty/${customerId}`);
const json = await res.json();
infoText.replaceChildren(json.loyaltySummary);
}

posBlockRow.append(infoText);
const posBlock = root.createComponent(POSBlock);

posBlock.append(posBlockRow);
root.append(posBlock);

getLoyaltyInfo();
});

Use direct API access when your extension needs to query or modify Shopify data in real-time. For example, you might want to update product metafields based on POS interactions, fetch detailed customer order history, or modify inventory levels during special promotions.

Make GraphQL Admin API requests directly from your extension with automatic authentication. These requests are fast because Shopify handles them directly without requiring a round trip to your backend.

Note

Direct API access is available as of POS version 10.6.0 for extensions targeting 2025-07 or later.

You must declare all required access scopes in your app's TOML file. For local development, access scopes are only registered or updated when the app is deployed and installed on your test store.

Query Shopify data directly

import {
reactExtension,
useApi,
POSBlock,
POSBlockRow,
Text,
} from '@shopify/ui-extensions-react/point-of-sale';
import type {DirectApiRequestBody} from '@shopify/ui-extensions/point-of-sale';
import React, {useEffect, useState} from 'react';

// This mutation requires the `write_products` access scope.
// https://shopify.dev/docs/api/admin-graphql/2025-07/mutations/metafieldsset
async function mutateMetafield(productId: number) {
const requestBody: DirectApiRequestBody = {
query: `#graphql
mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields {
key
namespace
value
createdAt
updatedAt
}
}
}
`,
variables: {
metafields: [
{
key: 'direct_api',
namespace: 'custom',
ownerId: `gid://shopify/Product/${productId}`,
value: 'Example Value',
},
],
},
};

await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
body: JSON.stringify(requestBody),
});
}

// https://shopify.dev/docs/api/admin-graphql/2025-07/queries/product
async function queryProductMetafields(productId: number) {
const requestBody: DirectApiRequestBody = {
query: `#graphql
query GetProduct($id: ID!) {
product(id: $id) {
id
metafields(first: 10) {
edges {
node {
id
namespace
key
value
}
}
}
}
}
`,
variables: {id: `gid://shopify/Product/${productId}`},
};
const res = await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
body: JSON.stringify(requestBody),
});
return res.json();
}

const ProductDetailsBlock = () => {
const {product} = useApi<'pos.product-details.block.render'>();
const [productInfo, setProductInfo] = useState<string>('');
useEffect(() => {
async function getProductInfo() {
const result = await queryProductMetafields(product.id);
setProductInfo(JSON.stringify(result, null, 2));
}
getProductInfo();
}, [product.id]);

return (
<POSBlock>
<POSBlockRow>
<Text>Metafields: {productInfo}</Text>
</POSBlockRow>
<POSBlockRow onPress={() => mutateMetafield(product.id)}>
<Text>Set metafield</Text>
</POSBlockRow>
</POSBlock>
);
};

export default reactExtension('pos.product-details.block.render', () => (
<ProductDetailsBlock />
));
import {
extension,
POSBlock,
POSBlockRow,
Text,
} from '@shopify/ui-extensions/point-of-sale';
import type {DirectApiRequestBody} from '@shopify/ui-extensions/point-of-sale';

// This mutation requires the `write_products` access scope.
// https://shopify.dev/docs/api/admin-graphql/2025-07/mutations/metafieldsset
async function mutateMetafield(productId: number) {
const requestBody: DirectApiRequestBody = {
query: `#graphql
mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields {
key
namespace
value
createdAt
updatedAt
}
}
}
`,
variables: {
metafields: [
{
key: 'direct_api',
namespace: 'custom',
ownerId: `gid://shopify/Product/${productId}`,
value: 'Example Value',
},
],
},
};

await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
body: JSON.stringify(requestBody),
});
}

// https://shopify.dev/docs/api/admin-graphql/2025-07/queries/product
async function queryProductMetafields(productId: number) {
const requestBody: DirectApiRequestBody = {
query: `#graphql
query GetProduct($id: ID!) {
product(id: $id) {
id
metafields(first: 10) {
edges {
node {
id
namespace
key
value
}
}
}
}
}
`,
variables: {id: `gid://shopify/Product/${productId}`},
};
const res = await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
body: JSON.stringify(requestBody),
});
return res.json();
}

export default extension('pos.product-details.block.render', (root, api) => {
const productId = api.product.id;
const metafieldInfoText = root.createComponent(Text);
const metafieldInfoRow = root.createComponent(POSBlockRow);

const setMetafieldText = root.createComponent(Text);
setMetafieldText.append('Set Metafield');

const setMetafieldRow = root.createComponent(POSBlockRow, {
onPress: () => mutateMetafield(productId),
});

async function getProductInfo() {
const result = await queryProductMetafields(productId);
metafieldInfoText.replaceChildren(JSON.stringify(result, null, 2));
}

metafieldInfoRow.append(metafieldInfoText);
setMetafieldRow.append(setMetafieldText);

const posBlock = root.createComponent(POSBlock);

posBlock.append(metafieldInfoRow);
posBlock.append(setMetafieldRow);
root.append(posBlock);

getProductInfo();
});

Anchor to Testing and deploymentTesting and deployment

After you've built your extension, test it thoroughly and deploy it to production.

Testing POS UI extensions requires a development store and the Shopify POS app on any mobile or tablet device—no dedicated POS hardware needed. Download the POS app from the App Store or Google Play, log in to your development store, and start testing.

Extensions run in preview mode during development, allowing you to test functionality and iterate quickly without affecting live merchant operations.

Use Shopify CLI to deploy your app and its extensions to production.

Note

Your compiled UI extension bundle can't exceed 64 KB. Shopify enforces this limit at deployment to ensure fast loading times and optimal performance. Learn how to analyze your bundle size.

Deploy your extension

shopify app deploy

Polaris reference docs follow Shopify's API versioning policy. Each stable version is supported for a minimum of 12 months. Older versions continue to work, they just won't have dedicated docs on Shopify.dev. Shopify CLI already prevents deploys targeting API versions older than 12 months, so we recommend keeping your extensions on a supported version.



Was this page helpful?