Sortboard
A searchable option area plus one or more bound placement zones you drag items between — a kanban board, a triage inbox, a seating chart, a tier list. Each item lives in exactly one place at a time. On every drop the board fires up to three events and writes the whole arrangement back through its synced value, so a server can seed the initial layout or push a new one in the same shape.
Drag is instant — touch a chip and it lifts and follows your finger right away (no long-press wait). A grip glyph marks what's moveable; a locked item or zone shows a lock badge, stays put, and rejects drops. Dropping between chips inserts by position; dropping into empty space appends.
Type
"sortboard"
Sizing (read this first)
The board manages its own internal scrolling, so it needs a real height or it collapses to one row. Two ways, matching the grid's mode:
mode: "flow"grid → setcontrolHeight(e.g.560). This is the simplest and what the bundled demo uses.mode: "grid"/2-D grid → give it a realspanrowSpan (e.g.[9, 4]);controlHeightis ignored in 2-D. See Grid Dimensions.
Config (sortboardConfig)
| Field | Type | Description |
|---|---|---|
items |
array | The option catalog. Each: { id, label?, icon?, tint?, zone?, locked? }. zone pre-places an item in that zone id (omitted ⇒ option area); locked: true pins it — it shows a lock badge and can't be dragged. |
zones |
array | The bins. Each: { id, label?, tint?, accepts?, capacity?, locked? }. accepts is a list of item ids the zone allows (omitted ⇒ any); capacity caps the count; locked: true fixes the whole column (nothing moves in or out). |
searchable |
bool | Filter field above the option area. |
paletteLabel |
string | Header for the option area (default "Options"). |
hidePalette |
bool | Hide the option area entirely — for boards whose items all start placed. |
columns |
number | Zone columns (default 2). |
events |
object | The pickup / place / layout actions (below). |
icon is any SF Symbol name; tint is a hex string.
Events (sortboardConfig.events)
Each is a standard action definition. An author payload receives
{{item}}, {{from}}, {{to}}, {{index}} token substitution; with no payload,
the tokens are sent as a flat object.
| Event | Fires | Default payload |
|---|---|---|
pickup |
An item is lifted / picked up | { "item", "from" } |
place |
An item is dropped into a zone | { "item", "from", "to", "index" } |
layout |
The board settles after a drop | { "zones": { zoneId: [itemId…] }, "changed": {…} } |
from/to are zone ids; the reserved id "palette" is the option area.
Accessibility
Under VoiceOver each chip is its own element: it reads its label and current zone,
and exposes a named "Move to
Synced value
The control's value is the arrangement JSON:
{ "zones": { "<zoneId>": ["<itemId>", …] } }. Point a Sync binding at a value in
this shape to let a server rearrange the board live. Unknown ids are dropped and any
declared item the server omits is parked back in the option area, so nothing silently
disappears.
Examples
Try it now: the bundled
sortboard-demolayout has both boards below, ready to drag. Everything here is copy-paste-ready into amode: "flow"tab.
1 — Sprint board (searchable kanban with a WIP limit)
{
"type": "sortboard",
"id": "sprint-board",
"position": [0, 0],
"span": [10, 4],
"controlHeight": 560,
"label": "Sprint Board",
"sortboardConfig": {
"searchable": true,
"paletteLabel": "Backlog",
"columns": 3,
"items": [
{ "id": "AUTH-14", "label": "OAuth refresh", "icon": "person.badge.key.fill", "tint": "#FF9500" },
{ "id": "UI-88", "label": "Dark mode", "icon": "moon.fill", "tint": "#667eea" },
{ "id": "PERF-5", "label": "Cold start", "icon": "bolt.fill", "tint": "#FF6B6B" },
{ "id": "DOC-9", "label": "API docs", "icon": "book.fill", "tint": "#5AC8FA", "zone": "doing" },
{ "id": "BUG-72", "label": "Crash on launch", "icon": "ant.fill", "tint": "#FF3B30", "zone": "done" }
],
"zones": [
{ "id": "todo", "label": "To Do" },
{ "id": "doing", "label": "In Progress", "capacity": 3, "tint": "#FF9500" },
{ "id": "done", "label": "Done", "tint": "#34C759" }
],
"events": {
"place": {
"method": "meshsocket", "mode": "send", "event": "route_msg",
"payload": { "target_id": "tracker", "type": "move_ticket",
"payload": { "ticket": "{{item}}", "column": "{{to}}", "index": "{{index}}" } }
},
"layout": { "method": "meshsocket", "mode": "send", "event": "board_state" }
}
}
}
2 — Seating chart (accepts + capacity)
{
"type": "sortboard",
"id": "seating-board",
"position": [0, 0],
"span": [10, 4],
"controlHeight": 560,
"label": "Table Assignments",
"sortboardConfig": {
"paletteLabel": "Guests",
"columns": 2,
"items": [
{ "id": "ada", "label": "Ada", "icon": "person.fill" },
{ "id": "linus", "label": "Linus", "icon": "person.fill" },
{ "id": "grace", "label": "Grace", "icon": "person.fill" }
],
"zones": [
{ "id": "table-1", "label": "Table 1", "capacity": 2, "tint": "#667eea" },
{ "id": "table-2", "label": "Table 2", "capacity": 2, "tint": "#FF9500" }
],
"events": {
"layout": { "method": "meshsocket", "mode": "send", "event": "seating_state" }
}
}
}
3 — Server-seeded board (no option area, driven by sync)
The board starts empty; the server pushes the arrangement in the synced shape and the user only rearranges between zones.
{
"type": "sortboard",
"id": "duty-roster",
"position": [0, 0],
"span": [8, 4],
"controlHeight": 460,
"label": "On-Call Roster",
"sortboardConfig": {
"hidePalette": true,
"columns": 2,
"items": [
{ "id": "sam", "label": "Sam", "icon": "person.fill" },
{ "id": "riley", "label": "Riley", "icon": "person.fill" }
],
"zones": [
{ "id": "primary", "label": "Primary", "capacity": 1, "tint": "#FF3B30" },
{ "id": "backup", "label": "Backup", "tint": "#34C759" }
],
"events": {
"place": { "method": "meshsocket", "mode": "send", "event": "route_msg",
"payload": { "target_id": "pager", "type": "assign",
"payload": { "who": "{{item}}", "role": "{{to}}" } } }
}
},
"sync": [
{ "method": "meshsocket", "type": "listen", "event": "broadcast",
"filter": { "kind": "roster" }, "valuePath": "state" }
]
}
A broadcast whose state is { "zones": { "primary": ["sam"], "backup": ["riley"] } }
seats everyone from the server.
Related
- Actions — Action definition and payload tokens
- Sync — Pushing an arrangement in from a server
- Grid Dimensions — Sizing (
controlHeightvs.span) - Control Definition — Base fields