Applies to api_version: "2026-01".
Worker UI extensions (blocks, modal/fullscreen actions, render: "worker" pages) build their UI only from the pos-* elements below. POS renders each element with its own components, so the UI always matches POS.
Iframe pages do not use this list; they use free-form HTML/React and can add @pancake-pos/ui to look like POS.
General rules
- Only use elements from this list. Plain HTML tags (
div,span,img,table,script…) and unknown elements are not rendered; the extension logs an error in the app's error log. - Text is only displayed inside
pos-text,pos-heading,pos-badge,pos-link,pos-button,pos-banner. - No
style,classNameor raw HTML. Colors, spacing and fonts follow POS. - URLs (
href,src) must behttps://. - Events pass values through
event.detail. In JSX useonClick,onChange… as with regular components. In plain JS, the event name is the part afteronin lowercase:onPageChange→addEventListener('pagechange', …). - Limits: 2,000 elements per extension;
pos-tablerenders at most 500 rows at a time (use pagination). - Array/object properties (
options,columns,rows,pagination) are passed directly in JSX; no JSON.stringify needed.
<pos-stack gap="small">
<pos-text-field label="Ticket code" value={code} onChange={(e) => setCode(e.detail)} />
<pos-button variant="primary" loading={saving} onClick={save}>Save</pos-button>
</pos-stack>Without a framework, build the tree with the runtime's DOM API (see Worker environment):
let code = '';
const field = document.createElement('pos-text-field');
field.label = 'Ticket code';
field.addEventListener('change', (e) => { code = e.detail; });
const button = document.createElement('pos-button');
button.variant = 'primary';
button.textContent = 'Save';
button.addEventListener('click', () => save(code));
const stack = document.createElement('pos-stack');
stack.gap = 'small';
stack.append(field, button);
document.body.append(stack);Layout
pos-stack
Stacks children vertically or horizontally.
| Property | Type | Default | Description |
|---|---|---|---|
direction |
'vertical' | 'horizontal' |
'vertical' |
Stacking direction |
gap |
'none' | 'small' | 'medium' | 'large' |
'medium' |
Space between children |
align |
'start' | 'center' | 'end' | 'stretch' |
'stretch' |
Cross-axis alignment |
justify |
'start' | 'center' | 'end' | 'space-between' |
'start' |
Main-axis alignment |
wrap |
boolean |
false |
Allow wrapping (with horizontal) |
pos-grid
A grid of equal columns; collapses to one column on narrow screens.
| Property | Type | Default | Description |
|---|---|---|---|
columns |
1–6 |
2 |
Number of columns |
gap |
'none' | 'small' | 'medium' | 'large' |
'medium' |
Spacing |
pos-section
A titled container, typically the outer shell of a block.
| Property | Type | Description |
|---|---|---|
heading |
string |
Title |
description |
string |
Short text below the title |
Slot actions: buttons shown at the right of the title.
<pos-section heading="Customer debt">
<pos-button slot="actions" variant="tertiary" onClick={reload}>Reload</pos-button>
<pos-text>...</pos-text>
</pos-section>pos-divider
A horizontal rule. No properties.
Display
pos-text
| Property | Type | Default |
|---|---|---|
tone |
'default' | 'subdued' | 'success' | 'warning' | 'critical' |
'default' |
size |
'small' | 'medium' | 'large' |
'medium' |
weight |
'regular' | 'semibold' | 'bold' |
'regular' |
pos-heading
| Property | Type | Default |
|---|---|---|
level |
1–4 |
3 |
pos-badge
A short status label.
| Property | Type | Default |
|---|---|---|
tone |
'default' | 'info' | 'success' | 'warning' | 'critical' |
'default' |
pos-money
Displays an amount using the shop's currency and number format.
| Property | Type | Description |
|---|---|---|
value |
number |
Amount |
currency |
string |
ISO 4217 code; defaults to the shop currency |
tone |
same as pos-text |
Text color |
pos-link
| Property | Type | Description |
|---|---|---|
href |
string |
https:// URL, always opens in a new tab |
onClick event: use it when the link performs an action in the app instead of opening a URL (omit href).
pos-image
| Property | Type | Default | Description |
|---|---|---|---|
src |
string |
An https:// URL on a domain listed in network.allowed_origins, or public/<file name> for a file in the project's public/ folder |
|
alt |
string |
Alternative text | |
width, height |
number |
Size (px) | |
fit |
'cover' | 'contain' |
'cover' |
How the image fits |
POS loads images through a proxy on the sandbox: image types only, up to 5 MB. Images from undeclared domains are not shown (development mode logs a warning).
Inputs
Properties shared by every input:
| Property | Type | Description |
|---|---|---|
label |
string |
Label |
value |
depends on the component | Current value (controlled component) |
disabled |
boolean |
Disable the input |
required |
boolean |
Show the required marker (does not validate) |
error |
string |
Error message below the input |
help |
string |
Hint below the input |
Shared events: onChange (event.detail is the new value), onBlur.
POS shows what the user types immediately and then sends onChange to the app, so inputs never lag while the app is busy. If the app sets a value different from what the user just entered (for example uppercasing it or clearing the field), POS shows the app's value. Set value directly in onChange; changing it after an await may overwrite what the user is typing.
pos-text-field
| Property | Type | Description |
|---|---|---|
value |
string |
|
placeholder |
string |
|
multiline |
boolean | number |
Multi-line input; a number sets the visible rows |
maxLength |
number |
Character limit |
pos-number-field
| Property | Type | Description |
|---|---|---|
value |
number | null |
|
min, max, step |
number |
|
suffix |
string |
Unit shown after the number, for example "%" |
pos-select
| Property | Type | Description |
|---|---|---|
options |
{ label: string; value: string; disabled?: boolean }[] |
Choices |
value |
string | string[] |
An array with multiple |
multiple |
boolean |
Multiple selection |
placeholder |
string |
|
searchable |
boolean |
Allow typing to filter |
pos-checkbox
| Property | Type | Description |
|---|---|---|
checked |
boolean |
|
label |
string |
onChange: event.detail is a boolean.
pos-date-picker
| Property | Type | Description |
|---|---|---|
value |
string | null |
Date as YYYY-MM-DD |
min, max |
string |
Selectable range, as YYYY-MM-DD |
Feedback
pos-banner
A prominent message inside the extension.
| Property | Type | Default | Description |
|---|---|---|---|
tone |
'info' | 'success' | 'warning' | 'critical' |
'info' |
|
heading |
string |
Title | |
dismissible |
boolean |
false |
Show a close button |
onDismiss event.
pos-spinner
| Property | Type | Default |
|---|---|---|
size |
'small' | 'medium' | 'large' |
'medium' |
Data and actions
pos-button
| Property | Type | Default | Description |
|---|---|---|---|
variant |
'primary' | 'secondary' | 'tertiary' | 'danger' |
'secondary' |
Button style |
size |
'small' | 'medium' |
'medium' |
|
loading |
boolean |
false |
Shows a busy state and blocks clicks |
disabled |
boolean |
false |
onClick event.
pos-table
A data table. Cells only hold values (strings, numbers); POS formats them according to the column format.
| Property | Type | Default | Description |
|---|---|---|---|
columns |
Column[] |
Column definitions | |
rows |
object[] |
[] |
Data, up to 500 rows |
rowKey |
string |
'id' |
Field used as the row key |
loading |
boolean |
false |
|
pagination |
{ page: number; pageSize: number; total: number } | null |
null |
App-side pagination |
emptyText |
string |
Content when there are no rows |
type Column = {
key: string; // field name in the row
title: string;
align?: 'left' | 'center' | 'right';
width?: number; // px
format?: 'text' | 'number' | 'money' | 'date' | 'datetime';
};Events:
onPageChange:event.detailis{ page, pageSize }.onRowClick:event.detailis the clicked row.
<pos-table
columns={[
{ key: 'system_id', title: 'Order' },
{ key: 'total_price', title: 'Total', format: 'money', align: 'right' },
]}
rows={orders}
rowKey="system_id"
pagination={{ page, pageSize: 20, total }}
onPageChange={(e) => setPage(e.detail.page)}
/>pos-modal
A dialog opened from inside an extension (for example from a button in a block).
| Property | Type | Description |
|---|---|---|
open |
boolean |
Open state |
heading |
string |
Title |
Slot footer for the dialog buttons. The onClose event fires when the user closes it or clicks outside.
<pos-modal open={open} heading="Confirm" onClose={() => setOpen(false)}>
<pos-text>Push this order to the ERP?</pos-text>
<pos-button slot="footer" onClick={() => setOpen(false)}>Cancel</pos-button>
<pos-button slot="footer" variant="primary" onClick={push}>Push</pos-button>
</pos-modal>Actions with presentation: "modal" already get a POS modal; do not wrap them in pos-modal.
Planned additions
pos-page, pos-tabs, pos-filter-bar, pos-chart (POS draws the chart, the app only passes data), for full-screen Worker UI pages.
