TileGrid
TileGrid renders a data array as a responsive, virtualized tile grid. It auto-calculates the number of columns based on the container width, tile width, and gap, and only renders visible rows — making it suitable for large datasets.
Key features:
- Responsive layout: Automatically calculates the number of columns from available width, tile width, and gap
- Virtualization: Only renders visible rows for smooth performance with large datasets
- Tile selection: Optional single or multi-tile selection with per-tile checkboxes
- Keyboard shortcuts: Built-in Ctrl/Cmd+A (select all), Delete, Cut, Copy, and Paste key bindings
- State sync: Keep selection synchronized with a global variable via
syncWithVar
Use TileGrid as a direct child of a container that provides an explicit height to enable virtualization.
In the following sections the examples use data with the structure outlined below:
| Id | Name | Category |
|---|---|---|
| 1 | Apples | fruits |
| 2 | Bananas | fruits |
| 3 | Carrots | vegetables |
| 4 | Spinach | vegetables |
| 5 | Milk | dairy |
| 6 | Cheese | dairy |
The data is provided inline. In source code samples, the data="{[...]}" declaration represents the data above.
All samples use a tile template with the following definition unless noted otherwise
(the ... nested into <TileGrid> represents this template definition):
<TileGrid data="{[...]}" itemWidth="120px" itemHeight="80px">
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
<Text color="gray">{$item.category}</Text>
</VStack>
</TileGrid>Context variables available during execution:
$isFirst:truewhen this is the first item in thedataarray.$isLast:truewhen this is the last item in thedataarray.$item: The current data item.$itemIndex: The zero-based index of the current item in the fulldataarray.$selected:truewhen this tile is currently selected.
Use children as Content Template
The itemTemplate property can be replaced by setting the item template component directly as the TileGrid's child. In the following example, the two TileGrid are functionally the same:
<App>
<!-- This is the same -->
<TileGrid>
<property name="itemTemplate">
<Text>Template</Text>
</property>
</TileGrid>
<!-- As this -->
<TileGrid>
<Text>Template</Text>
</TileGrid>
</App>Behaviors
This component supports the following behaviors:
| Behavior | Properties |
|---|---|
| Animation | animation, animationOptions |
| Bookmark | bookmark, bookmarkLevel, bookmarkTitle, bookmarkOmitFromToc |
| Display When | displayWhen |
| Component Label | label, labelPosition, labelWidth, labelBreak, required, enabled, shrinkToLabel, style, readOnly |
| Tooltip | tooltip, tooltipMarkdown, tooltipOptions |
| Styling Variant | variant |
Properties
checkboxPosition
default: "topStart"
Controls the position of the per-tile selection checkbox relative to the tile, respecting the current writing direction.
Available values:
| Value | Description |
|---|---|
topStart | Top of the tile, writing-direction start edge. (default) |
topEnd | Top of the tile, writing-direction end edge. |
bottomStart | Bottom of the tile, writing-direction start edge. |
bottomEnd | Bottom of the tile, writing-direction end edge. |
Controls where the per-tile selection checkbox is placed relative to the tile corner.
The position respects the current writing direction (inset-inline-start / inset-inline-end).
Available values: topStart (default), topEnd, bottomStart, bottomEnd.
<App>
<TileGrid
data="{[...]}"
itemWidth="120px"
itemHeight="80px"
itemsSelectable="true"
checkboxPosition="bottomEnd"
>
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
</VStack>
</TileGrid>
</App>data
The array of items to render as tiles. Each item is exposed as $item inside the tile template.
The data property accepts a static array of objects or a URL that resolves to a JSON array.
Each element of the array becomes a tile rendered by the itemTemplate.
<App>
<TileGrid
data="{[
{id: 1, name: 'Apples'},
{id: 2, name: 'Bananas'},
{id: 3, name: 'Carrots'}
]}"
itemWidth="120px"
itemHeight="80px"
>
<Text fontWeight="bold">{$item.name}</Text>
</TileGrid>
</App>enableMultiSelection
default: true
When true (default), multiple tiles can be selected using Ctrl/Cmd+Click or Shift+Click. When false, only a single tile can be selected at a time.
Controls whether multiple tiles can be selected simultaneously.
When false, clicking a tile deselects any previously selected tile.
This property only has an effect when itemsSelectable is true.
<App>
<TileGrid
data="{[...]}"
itemWidth="120px"
itemHeight="80px"
itemsSelectable="true"
enableMultiSelection="false"
>
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
</VStack>
</TileGrid>
</App>gap
default: "8px"
Gap between tiles, e.g. "8px" or a theme variable like "$gap-normal".
Specifies the space between tiles both horizontally and vertically.
Accepts any valid CSS length (e.g. "8px") or a theme variable (e.g. "$gap-normal").
<App>
<TileGrid data="{[...]}" itemWidth="120px" itemHeight="80px" gap="16px">
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
<Text color="gray">{$item.category}</Text>
</VStack>
</TileGrid>
</App>hideSelectionCheckboxes
default: false
If true, hides selection checkboxes. Selection logic still works via click, API, and keyboard.
If true, hides the per-tile selection checkboxes while keeping all selection logic intact.
Tiles can still be selected by clicking, via the API (selectId, selectAll), or with keyboard shortcuts.
This is useful when you want click-based selection without the visual checkbox overlay.
<App>
<TileGrid
data="{[...]}"
itemWidth="120px"
itemHeight="80px"
itemsSelectable="true"
hideSelectionCheckboxes="true"
>
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
</VStack>
</TileGrid>
</App>idKey
default: "id"
The property name used as the unique identifier for each item. Used to track selection state.
The property name used as the unique identifier for each data item.
Used to track and restore selection state. Defaults to "id".
<App>
<TileGrid
idKey="key"
itemsSelectable="true"
data="{[
{key: 'a', name: 'Alpha'},
{key: 'b', name: 'Beta'},
{key: 'c', name: 'Gamma'}
]}"
itemWidth="120px"
itemHeight="80px"
>
<Text fontWeight="bold">{$item.name}</Text>
</TileGrid>
</App>itemHeight
default: "140px"
Fixed height of each tile, e.g. "140px".
Specifies the fixed height of each tile as a CSS size value (e.g. "140px").
This value is also used as the virtualized row height, so it must be an explicit pixel value.
<App>
<TileGrid data="{[...]}" itemWidth="120px" itemHeight="120px">
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
<Text color="gray">{$item.category}</Text>
</VStack>
</TileGrid>
</App>itemsSelectable
default: false
Enables selection mode. When true, tiles can be selected by clicking on them.
Enables selection mode. When true, each tile shows a checkbox and can be selected by clicking.
The $selected context variable reflects the tile's selection state inside the template.
<App>
<TileGrid data="{[...]}" itemWidth="120px" itemHeight="80px" itemsSelectable="true">
<VStack
padding="8px"
horizontalAlignment="center"
verticalAlignment="center"
backgroundColor="{$selected ? '$color-primary-100' : 'transparent'}"
>
<Text fontWeight="bold">{$item.name}</Text>
</VStack>
</TileGrid>
</App>itemTemplate
The template used to render each tile. Use the $item context variable to access the current data item, $itemIndex for the zero-based index, and $selected to respond to the tile's selection state.
itemUserSelect
default: "none"
This property controls whether users can select text within tiles.
Available values:
| Value | Description |
|---|---|
auto | Default text selection behavior |
text | Text can be selected by the user |
none | Text cannot be selected (default) |
contain | Selection is contained within this element |
all | The entire element content is selected as one unit |
Controls whether users can select text within tiles.
Available values: auto (default text selection behavior), text (text can be selected), none (text cannot be selected), contain (selection is contained within this element), all (entire element content is selected as one unit).
<App>
<TileGrid
data="{[...]}"
itemWidth="120px"
itemHeight="80px"
itemUserSelect="text"
>
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
<Text color="gray">{$item.category}</Text>
</VStack>
</TileGrid>
</App>itemWidth
default: "120px"
Fixed width of each tile, e.g. "120px".
Specifies the fixed width of each tile as a CSS size value (e.g. "120px").
The number of columns is derived from this value, the available container width, and the gap.
<App>
<TileGrid data="{[...]}" itemWidth="160px" itemHeight="80px">
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
<Text color="gray">{$item.category}</Text>
</VStack>
</TileGrid>
</App>loading
default: false
When true, the grid shows a placeholder loading state instead of tile content.
When true, the grid hides all tile content and shows an empty placeholder.
Use this while data is being fetched to prevent a layout flash.
<App>
<TileGrid
loading="true"
itemWidth="120px"
itemHeight="80px"
>
<Text>{$item.name}</Text>
</TileGrid>
</App><App>
<TileGrid
loading="true"
itemWidth="120px"
itemHeight="80px"
>
<Text>{$item.name}</Text>
</TileGrid>
</App>refreshOn
An optional value that, when changed, forces all visible tiles to re-render so their XMLUI event-handler closures pick up the latest reactive state. Bind to any global variable whose change should invalidate tile closures (e.g. "{selectMode}"). If not provided, tiles re-render on every XMLUI reactive cycle.
stretchItems
default: false
When true, tiles in each row grow to fill the full container width. itemWidth becomes the minimum tile width; the actual width is distributed evenly.
When true, tiles grow to fill the full container width.
The number of columns is still derived from itemWidth, but each tile's actual width is distributed evenly across the available space, eliminating trailing horizontal gaps.
itemWidth acts as the minimum tile width that determines the column count.
<App>
<TileGrid data="{[...]}" itemWidth="120px" itemHeight="80px" stretchItems="true">
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
<Text color="gray">{$item.category}</Text>
</VStack>
</TileGrid>
</App>syncWithVar
The name of a global variable to synchronize the grid's selection state with. The named variable must reference an object; the grid will read from and write to its selectedIds property. A runtime error is signalled if the value is not a valid JavaScript variable name.
The following example demonstrates two TileGrid components sharing selection state through a global variable.
Selecting a tile in either grid immediately reflects in the other:
syncWithVarworks with both global and local variables. When using local variables, ensure all TileGrids in the sync have that variable in scope.
<App global.selState="{{}}">
<MyGrid />
<Text>Selection: {JSON.stringify(selState)}</Text>
<MyGrid />
</App><Component name="MyGrid">
<TileGrid
syncWithVar="selState"
itemsSelectable="true"
data="{[
{id: 1, name: 'Apples'},
{id: 2, name: 'Bananas'},
{id: 3, name: 'Carrots'}
]}"
itemWidth="120px"
itemHeight="80px"
>
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
</VStack>
</TileGrid>
</Component>Change the selection in one of the grids and check how it is synced.
<App global.selState="{{}}">
<MyGrid />
<Text>Selection: {JSON.stringify(selState)}</Text>
<MyGrid />
</App><Component name="MyGrid">
<TileGrid
syncWithVar="selState"
itemsSelectable="true"
data="{[
{id: 1, name: 'Apples'},
{id: 2, name: 'Bananas'},
{id: 3, name: 'Carrots'}
]}"
itemWidth="120px"
itemHeight="80px"
>
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
</VStack>
</TileGrid>
</Component>Change the selection in one of the grids and check how it is synced.
toggleSelectionOnClick
default: false
When true, a plain click toggles the tile's selection state instead of replacing the current selection. Ctrl+Click and Shift+Click behavior is unchanged. Only has an effect when itemsSelectable is true.
When true, a plain click toggles the tile's selection state (adds it if not selected, removes it if already selected) instead of replacing the current selection.
This property only has an effect when itemsSelectable is true. Ctrl+Click and Shift+Click behavior is unchanged.
The default value is false.
<App>
<TileGrid
data="{[...]}"
itemWidth="120px"
itemHeight="80px"
itemsSelectable="true"
toggleSelectionOnClick="true"
>
<VStack padding="8px" horizontalAlignment="center" verticalAlignment="center">
<Text fontWeight="bold">{$item.name}</Text>
</VStack>
</TileGrid>
</App>Events
contextMenu
Fired when a tile is right-clicked. Receives the tile data item as $item and its zero-based index as $itemIndex.
Fired when a tile is right-clicked. Receives the tile data item as $item and its zero-based index as $itemIndex.
<App var.testState="">
<TileGrid
data="{[
{id: 1, name: 'Apples'},
{id: 2, name: 'Bananas'},
{id: 3, name: 'Carrots'}
]}"
itemWidth="120px"
itemHeight="80px"
onContextMenu="testState = $item.name"
>
<Text>{$item.name}</Text>
</TileGrid>
<Text>Right-clicked: {testState}</Text>
</App>copyAction
Fired when the user presses Ctrl/Cmd+C. Receives (focusedItem, selectedItems, selectedIds).
Fired when the user presses Ctrl/Cmd+C while the grid is focused and itemsSelectable is true.
The handler receives (focusedItem, selectedItems, selectedIds).
(See the deleteAction event for the general pattern.)
cutAction
Fired when the user presses Ctrl/Cmd+X. Receives (focusedItem, selectedItems, selectedIds).
Fired when the user presses Ctrl/Cmd+X while the grid is focused and itemsSelectable is true.
The handler receives (focusedItem, selectedItems, selectedIds).
(See the deleteAction event for the general pattern.)
deleteAction
Fired when the user presses the Delete key. Receives (focusedItem, selectedItems, selectedIds).
Fired when the user presses the Delete key while the grid is focused and itemsSelectable is true.
The handler receives (focusedItem, selectedItems, selectedIds).
The component does not remove items automatically — the handler must implement the removal.
<App var.msg="">
<Text>{msg}</Text>
<TileGrid
data="{[
{id: 1, name: 'Apples'},
{id: 2, name: 'Bananas'},
{id: 3, name: 'Carrots'}
]}"
itemWidth="120px"
itemHeight="80px"
itemsSelectable="true"
onDeleteAction="(focused, items, ids) => msg = 'Delete pressed for: ' + ids.join(', ')"
>
<Text fontWeight="bold">{$item.name}</Text>
</TileGrid>
</App><App var.msg="">
<Text>{msg}</Text>
<TileGrid
data="{[
{id: 1, name: 'Apples'},
{id: 2, name: 'Bananas'},
{id: 3, name: 'Carrots'}
]}"
itemWidth="120px"
itemHeight="80px"
itemsSelectable="true"
onDeleteAction="(focused, items, ids) => msg = 'Delete pressed for: ' + ids.join(', ')"
>
<Text fontWeight="bold">{$item.name}</Text>
</TileGrid>
</App>itemDoubleClick
Fired when a tile is double-clicked. Receives the data item.
Fired when a tile is double-clicked. The handler receives the tile's data item.
<App var.last="">
<Text>Last double-clicked: {last}</Text>
<TileGrid
data="{[...]}"
itemWidth="120px"
itemHeight="80px"
onItemDoubleClick="(item) => last = item.name"
>
<Text fontWeight="bold">{$item.name}</Text>
</TileGrid>
</App>pasteAction
Fired when the user presses Ctrl/Cmd+V. Receives (focusedItem, selectedItems, selectedIds).
Fired when the user presses Ctrl/Cmd+V while the grid is focused and itemsSelectable is true.
The handler receives (focusedItem, selectedItems, selectedIds).
(See the deleteAction event for the general pattern.)
selectAllAction
Fired when the user presses Ctrl/Cmd+A. Receives (selectedItems, selectedIds).
Fired when the user presses Ctrl/Cmd+A while the grid is focused and itemsSelectable is true.
The component automatically selects all tiles before invoking this handler.
The handler receives (selectedItems, selectedIds).
<App var.msg="">
<Text>{msg}</Text>
<TileGrid
data="{[
{id: 1, name: 'Apples'},
{id: 2, name: 'Bananas'},
{id: 3, name: 'Carrots'}
]}"
itemWidth="120px"
itemHeight="80px"
itemsSelectable="true"
onSelectAllAction="(items, ids) => msg = 'Selected all: ' + ids.join(', ')"
>
<Text fontWeight="bold">{$item.name}</Text>
</TileGrid>
</App><App var.msg="">
<Text>{msg}</Text>
<TileGrid
data="{[
{id: 1, name: 'Apples'},
{id: 2, name: 'Bananas'},
{id: 3, name: 'Carrots'}
]}"
itemWidth="120px"
itemHeight="80px"
itemsSelectable="true"
onSelectAllAction="(items, ids) => msg = 'Selected all: ' + ids.join(', ')"
>
<Text fontWeight="bold">{$item.name}</Text>
</TileGrid>
</App>selectionDidChange
Fired when the selection changes. Receives the array of currently selected items.
Fired when the set of selected tiles changes. The handler receives an array of the currently selected data items.
<App var.selected="">
<Text>Selected: {selected}</Text>
<TileGrid
data="{[...]}"
itemWidth="120px"
itemHeight="80px"
itemsSelectable="true"
onSelectionDidChange="(items) => selected = items.map(i => i.name).join(', ')"
>
<Text fontWeight="bold">{$item.name}</Text>
</TileGrid>
</App>Exposed Methods
This component does not expose any methods.
Styling
The visual appearance of TileGrid can be customized through theme variables. These control layout, spacing, and typography for tiles and their selection controls.
| Theme variable | Description |
|---|---|
fontSize-checkbox-TileGrid | Font size of the checkbox label used for tile selection. |
Theme Variables
| Variable | Default Value (Light) | Default Value (Dark) |
|---|---|---|
| backgroundColor-item-TileGrid | transparent | transparent |
| backgroundColor-item-TileGrid--hover | $color-surface-100 | $color-surface-100 |
| backgroundColor-item-TileGrid--selected | $color-surface-100 | $color-surface-100 |
| backgroundColor-item-TileGrid--selected--hover | $color-primary-100 | $color-primary-100 |
| borderRadius-item-TileGrid | $borderRadius | $borderRadius |
| fontSize-checkbox-TileGrid | $fontSize | $fontSize |
| offsetHorizontal-checkbox-TileGrid | 4px | 4px |
| offsetVertical-checkbox-TileGrid | 4px | 4px |
| outlineColor-item-TileGrid--focus | $color-primary-500 | $color-primary-500 |
| outlineOffset-item-TileGrid--focus | -2px | -2px |
| outlineStyle-item-TileGrid--focus | solid | solid |
| outlineWidth-item-TileGrid--focus | 2px | 2px |
| userSelect-item-TileGrid | none | none |