---
title: POS list
description: >-
  The POS list component displays structured data in rows, using item templates
  composed of other web components. Use it to present large collections with
  consistent formatting and row interactions.
api_version: 2026-10
source_url:
  html: >-
    https://shopify.dev/docs/api/pos-ui-extensions/latest/web-components/layout-and-structure/pos-list
  md: >-
    https://shopify.dev/docs/api/pos-ui-extensions/latest/web-components/layout-and-structure/pos-list.md
api_name: pos-ui-extensions
---

# POS list

The POS list component displays structured data in rows, using item templates composed of other web components. Use it to present large collections with consistent formatting and row interactions.

### Support Targets (8)

### Supported targets

* [pos.​cart.​line-item-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-10/targets/cart-details#cart-details-action-modal-)
* [pos.​customer-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-10/targets/customer-details#customer-details-action-modal-)
* [pos.​draft-order-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-10/targets/draft-order-details#draft-order-details-action-modal-)
* [pos.​home.​modal.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-10/targets/home-screen#home-screen-action-modal-)
* [pos.​order-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-10/targets/order-details#order-details-action-modal-)
* [pos.​product-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-10/targets/product-details#product-details-action-modal-)
* [pos.​purchase.​post.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-10/targets/post-purchase#post-purchase-action-modal-)
* [pos.​register-details.​action.​render](https://shopify.dev/docs/api/pos-ui-extensions/2026-10/targets/register-details#register-details-action-modal-)

#### Use cases

* **Object display:** Display and link to records such as cart items, customers, or products, with images and descriptions.
* **Menu items:** Display menu items and settings with switches, or a chevron icon to indicate navigation.
* **Selectable options:** Display selectable options, filters, and actions.
* **Mixed layouts:** Combine section headers, dividers, and content rows in one list by giving each kind of row its own template.
* **Paginated data:** Show paginated data with a loading indicator and an end-reached callback.

***

## Properties

Displays a virtualized list of rows rendered from plain data and item templates compiled with `posListTemplate`.

* **id**

  **string**

  A unique identifier for the element.

* **item​Templates**

  **POSListTemplates**

  Reusable templates that define the layout of list rows. Use `posListTemplate` to define one or more `<s-pos-list-item>` templates.

* **loading​More**

  **boolean**

  **Default: false**

  Whether more data is being loaded. Set to `true` when paginating and fetching additional data for the list. POS shows a progress indicator after the last row.

* **rows**

  **POSListRow\[]**

  An array of POSListRow objects that define the content of each row in the list.

### POSListTemplates

* templates

  ```ts
  POSListItemTemplate[]
  ```

### POSListItemTemplate

One \`\<s-pos-list-item templateId="…">\` root.

* children

  ```ts
  POSListTemplateNode[]
  ```

* templateId

  ```ts
  string
  ```

* type

  \`button\` rows fire \`rowclick\`; \`text\` rows are static content.

  ```ts
  'button' | 'text'
  ```

### POSListTemplateNode

```ts
{
      kind: 'element';
      tag: string;
      props?: Record<string, POSListTemplateProp>;
      children?: POSListTemplateNode[];
    } | {
      kind: 'text';
      segments: POSListTemplateSegment[];
    } | {
      kind: 'if';
      path: string;
      children: POSListTemplateNode[];
    }
```

### POSListTemplateProp

```ts
{
      kind: 'literal';
      value: string | boolean;
    } | {
      kind: 'field';
      path: string;
    } | {
      kind: 'segments';
      segments: POSListTemplateSegment[];
    }
```

### POSListTemplateSegment

A literal string piece or a row-field lookup within interpolated text.

```ts
string | {
      path: string;
    }
```

### POSListRow

A row supplied to \`s-pos-list\`. Rows are plain data; every member other than \`id\` and \`templateFor\` is available to the row's item template through \`{{path}}\`, \`bind:prop\`, and \`{{#if path}}\` bindings.

* \[field: string]

  ```ts
  unknown
  ```

* id

  A unique identifier for the row. Keys virtualization and identity across incremental loads.

  ```ts
  string
  ```

* templateFor

  The \`templateId\` of the item template that renders this row.

  ```ts
  string
  ```

### Events

The POS list component provides event callbacks for handling user interactions. Learn more about [handling events](https://shopify.dev/docs/api/polaris/using-polaris-web-components#handling-events).

* **loadmore**

  **(event: CallbackEvent<"s-pos-list">) => void**

  Callback when the list has scrolled near its end and more rows should be loaded.

* **rowclick**

  **(event: POSListRowClickEvent) => void**

  Callback when a `button` row is activated. `event.detail.item` is the row and `event.detail.index` its position in `rows`. Rows rendered by a `text` template never fire it.

### CallbackEvent

* bubbles

  ```ts
  boolean
  ```

* cancelable

  ```ts
  boolean
  ```

* composed

  ```ts
  boolean
  ```

* currentTarget

  ```ts
  HTMLElementTagNameMap[T]
  ```

* detail

  ```ts
  any
  ```

* eventPhase

  ```ts
  number
  ```

* target

  ```ts
  HTMLElementTagNameMap[T] | null
  ```

### POSListRowClickEvent

The event fired when a \`button\` row is activated. \`event.detail.item\` is the activated row, and \`event.detail.index\` is its position in \`rows\`.

```ts
CallbackEvent<'s-pos-list'> & {
  detail: {
    item: POSListRow;
    index: number;
  };
}
```

### Slots

Content slots for the POS list.

* **header**

  **HTMLElement**

  Content displayed before the rows as part of the list's scrollable content.

***

## Template

Each kind of row is laid out by a template. Write each template as an `<s-pos-list-item>` element, compile the templates with `posListTemplate`, and pass the result to `itemTemplates`.

### Import pos​List​Template

```jsx
import {posListTemplate} from '@shopify/ui-extensions/point-of-sale';
```

### Define templates

Call `posListTemplate` as a tagged template, with one `<s-pos-list-item>` for each kind of row. Inside it, use the same components as the rest of your extension. Define templates outside your component, at the top level of the file, so they're compiled once instead of on every render.

```jsx
const itemTemplates = posListTemplate`
  <s-pos-list-item templateId="section-header" type="text">
    <s-heading>{{title}}</s-heading>
  </s-pos-list-item>


  <s-pos-list-item templateId="product">
    <s-stack direction="inline" gap="base" alignItems="center">
      <s-box inlineSize="48px" blockSize="48px">
        <s-image bind:src="image.src" alt="Photo of {{name}}" objectFit="cover" />
      </s-box>
      <s-stack gap="small-200">
        <s-text>{{name}}</s-text>
        <s-text color="subdued">{{variant}} · {{price}}</s-text>
        {{#if onSale}}<s-badge tone="success">Sale</s-badge>{{/if}}
      </s-stack>
      <s-switch bind:checked="active" label="Available" />
    </s-stack>
  </s-pos-list-item>
`;
```

### List item attributes

| Attribute | Required | Description |
| - | - | - |
| `templateId` | Yes | A unique name for the template. Rows select a template by this name. |
| `type` | No | `button` (the default): the row is one button and fires `rowclick`. `text`: the row is static content with no button role. |

`<s-pos-list-item>` takes no other attributes, and it must be at the top level of the template, not inside another element.

### Connect rows to templates

Each row's `templateFor` is the `templateId` of the template that renders it, and the row's fields fill the template's placeholders: `{{name}}` shows the row's `name`. Every row also needs a unique string `id`.

```jsx
const rows = [
  {id: 'summer', templateFor: 'section-header', title: 'Summer collection'},
  {
    id: 'graphic-tee',
    templateFor: 'product',
    name: 'Graphic tee',
    variant: 'Black / Medium',
    price: '$29.00',
    image: {src: 'placeholder.png'},
    onSale: true,
    active: true,
  },
  {
    id: 'canvas-tote',
    templateFor: 'product',
    name: 'Canvas tote',
    variant: 'Natural',
    price: '$18.00',
    image: {src: 'placeholder.png'},
    onSale: false,
    active: false,
  },
];


<s-pos-list rows={rows} itemTemplates={itemTemplates} />
```

| Row `id` | `templateFor` | Rendered by |
| - | - | - |
| `summer` | `section-header` | `<s-pos-list-item templateId="section-header">` |
| `graphic-tee` | `product` | `<s-pos-list-item templateId="product">` |
| `canvas-tote` | `product` | `<s-pos-list-item templateId="product">` |

Several rows can share a template. A row whose `templateFor` matches no `templateId` renders nothing.

### Paths

A path names a field on the row. Use dots to reach nested fields. Take the `graphic-tee` row:

```jsx
{
  id: 'graphic-tee',
  templateFor: 'product',
  name: 'Graphic tee',
  variant: 'Black / Medium',
  price: '$29.00',
  image: {src: 'placeholder.png'},
  onSale: true,
  active: true,
}
```

| Path | Value in this row |
| - | - |
| `name` | `'Graphic tee'` |
| `image.src` | `'placeholder.png'` |
| `onSale` | `true` |
| `active` | `true` |

A path that doesn't exist on the row resolves to `undefined`: text and attributes show it as empty, and `{{#if}}` hides its content.

**Note:** Paths can use letters (A–Z and a–z), digits, `_`, and dots, which is narrower than JavaScript property names. A field such as `sale-price`, `$id`, or `café` can't be read, so name row fields like `salePrice`.

### Template syntax

| Syntax | Example | What it does |
| - | - | - |
| `{{path}}` in text | `<s-text>{{name}}</s-text>` | Shows the field as text: `Graphic tee`. |
| `{{path}}` in an attribute | `alt="Photo of {{name}}"` | Builds a string from text and fields: `"Photo of Graphic tee"`. **Note:** The result is always a string. On a boolean prop, any non-empty string counts as `true`, so `checked="{{active}}"` turns the switch on even when `active` is `false`. Use `bind:checked="active"` instead. |
| `bind:prop="path"` | `<s-switch bind:checked="active" />` | Passes the field's value with its own type: the switch gets `true`. Use it for booleans, numbers, and arrays. |
| `{{#if path}}…{{/if}}` | `{{#if onSale}}<s-badge tone="success">Sale</s-badge>{{/if}}` | Shows the content only when the field is truthy: `graphic-tee` shows the badge and `canvas-tote` doesn't. Blocks can't nest, and there's no `else`. |

### Common errors

* **An invalid template stops your extension from loading.** `posListTemplate` checks templates when your extension runs, not when you build it, so the build succeeds. When a template is invalid, POS shows a generic "Error loading extension" message, not the problem `posListTemplate` found. Templates are static, so an invalid template fails every time: after you change a template, open your extension with `shopify app dev` and fix any error before you release.
* **A row with a non-string `id` or no `templateFor` replaces your extension with an error.** POS APIs return numeric IDs, so convert them, for example `id: product.id.toString()`. A row whose `id` repeats another row's may not render, and POS doesn't report it.
* **Rows without `itemTemplates` render nothing.** If `itemTemplates` isn't set, the list shows no rows and POS doesn't report an error. The header and loading indicator still render.

***

## Examples

### Display rows with item templates

Define a `section-header` template for static headings and a `product` template for tappable rows, and keep the rows in state. Tapping a product row fires `onRowClick`; the handler finds the row from `event.detail.item` and flips its `active` field, which updates the switch bound to it. A banner in the `header` slot tells the POS user what tapping does.

## Display rows with item templates

![A POS list with an info banner that reads Tap a product to change its availability, a Summer collection heading, and two product rows: Graphic tee, on sale with its availability switch on, and Canvas tote with its switch off.](https://shopify.dev/assets/assets/images/templated-apis-screenshots/pos-ui-extensions/2026-10/pos-list-default-DWMehzTy.png)

## Display rows with item templates

```tsx
import {posListTemplate} from '@shopify/ui-extensions/point-of-sale';
import {render} from 'preact';
import {useState} from 'preact/hooks';


const itemTemplates = posListTemplate`
  <s-pos-list-item templateId="section-header" type="text">
    <s-heading>{{title}}</s-heading>
  </s-pos-list-item>


  <s-pos-list-item templateId="product">
    <s-stack direction="inline" gap="base" alignItems="center">
      <s-box inlineSize="48px" blockSize="48px">
        <s-image bind:src="image.src" alt="Photo of {{name}}" objectFit="cover" />
      </s-box>
      <s-stack gap="small-200">
        <s-text>{{name}}</s-text>
        <s-text color="subdued">{{variant}} · {{price}}</s-text>
        {{#if onSale}}<s-badge tone="success">Sale</s-badge>{{/if}}
      </s-stack>
      <s-switch bind:checked="active" label="Available" />
    </s-stack>
  </s-pos-list-item>
`;


const initialRows = [
  {id: 'summer', templateFor: 'section-header', title: 'Summer collection'},
  {
    id: 'graphic-tee',
    templateFor: 'product',
    name: 'Graphic tee',
    variant: 'Black / Medium',
    price: '$29.00',
    image: {src: 'placeholder.png'},
    onSale: true,
    active: true,
  },
  {
    id: 'canvas-tote',
    templateFor: 'product',
    name: 'Canvas tote',
    variant: 'Natural',
    price: '$18.00',
    image: {src: 'placeholder.png'},
    onSale: false,
    active: false,
  },
];


function Extension() {
  const [rows, setRows] = useState(initialRows);


  // The switch can't be tapped on its own: tapping anywhere on the row fires
  // onRowClick, so update the row's `active` field to change the switch.
  const toggleAvailability = (id: string) =>
    setRows((currentRows) =>
      currentRows.map((row) =>
        row.id === id && row.active !== undefined
          ? {...row, active: !row.active}
          : row,
      ),
    );


  return (
    <s-pos-list
      rows={rows}
      itemTemplates={itemTemplates}
      onRowClick={(event) => toggleAvailability(event.detail.item.id)}
    >
      <s-banner
        slot="header"
        heading="Tap a product to change its availability"
        tone="info"
      />
    </s-pos-list>
  );
}


export default async () => {
  render(<Extension />, document.body);
};
```

### Load rows incrementally

POS calls `onLoadMore` when the POS user scrolls near the end of the list. Append the next page of rows and set `loadingMore` while it loads, so the list shows a loading indicator. Ignore calls while a page is loading, and stop once every row is loaded. Here a one-second timer stands in for a real request.

## Load rows incrementally

![A POS list of orders from Order #1007 to Order #1015. Each row has an image, the order number, a blue Odd or green Even badge, and a right chevron. A loading indicator is below the last row.](https://shopify.dev/assets/assets/images/templated-apis-screenshots/pos-ui-extensions/2026-10/pos-list-load-rows-incrementally-B0eO0Mxp.png)

## Load rows incrementally

```tsx
import {posListTemplate} from '@shopify/ui-extensions/point-of-sale';
import {render} from 'preact';
import {useState} from 'preact/hooks';


const PAGE_SIZE = 16;
const TOTAL_ORDERS = 80;


const itemTemplates = posListTemplate`
  <s-pos-list-item templateId="order">
    <s-stack direction="inline" justifyContent="space-between" alignItems="center">
      <s-stack direction="inline" gap="base" alignItems="center">
        <s-box inlineSize="48px" blockSize="48px">
          <s-image src="placeholder.png" alt="" objectFit="cover" />
        </s-box>
        <s-text>{{name}}</s-text>
        <s-badge tone="{{badgeTone}}">{{parity}}</s-badge>
      </s-stack>
      <s-icon type="chevron-right" />
    </s-stack>
  </s-pos-list-item>
`;


function createOrders(start: number) {
  return Array.from({length: PAGE_SIZE}, (_, offset) => {
    const index = start + offset;
    const orderNumber = 1000 + index;
    const even = index % 2 === 0;
    return {
      id: String(orderNumber),
      templateFor: 'order',
      name: `Order #${orderNumber}`,
      parity: even ? 'Even' : 'Odd',
      badgeTone: even ? 'success' : 'info',
    };
  });
}


function Extension() {
  const [rows, setRows] = useState(() => createOrders(0));
  const [loadingMore, setLoadingMore] = useState(false);


  const loadMore = async () => {
    if (loadingMore || rows.length >= TOTAL_ORDERS) return;


    setLoadingMore(true);
    // Stands in for a real request.
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setRows([...rows, ...createOrders(rows.length)]);
    setLoadingMore(false);
  };


  return (
    <s-pos-list
      rows={rows}
      itemTemplates={itemTemplates}
      loadingMore={loadingMore}
      onLoadMore={loadMore}
      onRowClick={(event) => {
        // Navigate to the tapped order here, using event.detail.item.
      }}
    />
  );
}


export default async () => {
  render(<Extension />, document.body);
};
```

***

## Best practices

* **Compile templates once:** Define templates outside your component, at the top level of the file, so they're compiled once instead of on every render.
* **Check new templates in POS:** TypeScript doesn't check the markup inside a template. Unknown tags are skipped, but their children still render; props a component doesn't declare are ignored; and for props with a fixed set of allowed values, an unsupported value falls back to the prop's default.
* **Use unique string row IDs:** Give each row a unique string `id` that stays the same when you append or update rows. Convert numeric IDs from POS APIs, for example `product.id.toString()`.
* **Put display-ready values in rows:** Format dates, prices, and other values as strings before you add them to a row. A `Date`, `Map`, `Set`, or class instance doesn't render as readable text.
* **Keep rows flat:** Put the fields a template reads directly on the row, or one level deep, and give them the type the component expects. `bind:prop` passes values through unchanged, so a string `"false"` stays truthy.
* **Prefer `bind:` for non-string props:** Use `{{path}}` for text and string attributes, and `bind:prop` whenever the component expects a boolean, number, or array.
* **Prevent duplicate loads:** Ignore `onLoadMore` events while a request is already in progress or after you reach the final page.
* **Keep interactions consistent:** Every row rendered by a `button` template is one button. Use `type="text"` for headings, dividers, totals, and other informational rows so they don't announce as buttons.
* **Place headings based on scroll behavior:** Put a page-level heading outside the list. Use the `header` slot when the heading, a search field, or a banner should scroll with the rows.
* **Don't nest long lists in a scroll box:** `s-pos-list` scrolls on its own. A short list inside `s-scroll-box` works, but place long or incrementally loaded lists directly on the screen rather than inside another scrolling container.

***

## Limitations

* Nothing inside a row is individually interactive: a row is either one button or static content. `rowclick` fires on the list, with `event.currentTarget` set to the `s-pos-list` element and the activated row and its index in `event.detail`. To change a switch in a row, update the row's data.
* `rowclick` and `loadmore` are element events. An error thrown or a promise rejected in the listener stays in your extension and isn't reported by POS as an extension error, so handle failures in the listener.
* Templates are static and compiled by `posListTemplate`. You can't interpolate values into a template or change a template per row; use `{{#if}}` and separate templates for variations.
* `{{#if}}` blocks can't nest and have no `else`. `{{else}}` isn't a branch: like any other `{{path}}`, it reads a row field, here one named `else`. Precompute derived flags on the row instead.
* A row's layout uses the same components as the rest of your extension, so a template can't layer or position elements, for example to place a badge over an image.

***
