Cards
The card document as a tree of elements — the six element types, the one that binds to your data, and how rules and overrides layer on top.
A card is one document describing one small page: a tree of elements, in order, with style and handlers attached wherever you want them.
{
"version": 2,
"style": { "gap": 6, "borderRadius": 12, "padding": { "x": 14, "y": 10 } },
"on": { "click": [ { "command": "openPeek" } ] },
"elements": [
{ "type": "text", "text": "Morning run", "style": { "fontWeight": "semibold" } },
{ "type": "field", "property": "distance_km", "display": "stat" },
{ "type": "group", "style": { "flexDirection": "row", "gap": 8 }, "elements": [
{ "type": "icon", "icon": "lucide:timer" },
{ "type": "field", "property": "elapsed", "control": "stopwatch" }
] }
]
}
Nothing about that shape is card-specific. The same tree, the same style keys, the same events and commands describe a list row, a dashboard widget or a board — which is why moving a design from one to another is mostly a copy and paste.
The six element types
| Type | Is | Carries |
|---|---|---|
field | a bound cell — it names one of your properties | property, control, display, options, label, style, on |
button | a control whose whole meaning is its handler | label, icon, style, on |
text | words you wrote | text, style, on |
icon | an emoji, or an icon by name | icon, style, on |
image | a picture you attached | media, style, on |
group | a nestable box | elements, style, on |
field is the only element that binds
Everything else is ornament. text, icon and image show what you put in them; only a field names a real property and shows the value stored on the entry. That distinction is the reason a card can be checked: name a property that no longer exists and the app can tell you, by name, which card is broken.
Ornament is still allowed to act. An icon can carry an on handler and run a command, and that is not a back door — it is the ordinary, recorded command path, the same one a button uses. What ornament cannot do is bind.
control vs display
A field shows a value, and you choose how.
controlpicks the editor — how you change the value in place.input,select,radio,checkbox,stopwatch,map,markdown, and so on. The list available to a field depends on the property's kind: you cannot put a stopwatch on a text property.displaypicks an alternate presentation — how the value reads when you are not editing it.statfor a big number,heroorcoverfor an image that fills the card,thumbfor a small one,tracefor a route,valuefor the plain reading.
Set neither and the field renders the ordinary way for its kind, which is usually what you want.
Rating fields
A field on a Rating property takes four control words:
rating— the property's own "Rate with" parts (the default).icons— only the icons row.range— only the slider.input— only the number box with − and +.
A rating field may also carry options, which swap the icon (and label or colour) of an existing scale value on this card only:
{ "type": "field", "property": "mood", "control": "icons",
"options": [{ "value": 1, "icon": "😣" }, { "value": 5, "icon": "😄" }] }
options does not add or remove values — the scale stays the property's. Up to 20 entries.
Handlers, rules and ids
on may sit at any level, and the nearest handler wins — see Events.
Rules are the conditional layer: an expression decides whether an element shows, or which style it takes, based on the entry's own values. That is where if lives; a handler itself is always a flat list.
id is optional. You only need one on an element that something else has to point at — a rule, or an override.
Overrides, and what wins
The same card can look slightly different in different places, and it does that by layering rather than by forking. Your theme is the bottom layer, so a card that says nothing already looks right. On top of that, more specific layers win: the collection's own card, then a particular view's version of it, then a preset's, then a rule that only applies on some entries, then a narrow-screen adjustment last of all.
The practical consequence is that you should say as little as possible. Every key you set is a key that stops following your theme.
Versions
Every card carries a version, and older cards are upgraded as they load. A card you designed before the language grew a word keeps rendering exactly as it did.
Owners and inheritance
Cards resolve independently for each type: app defaults, collection, view, then preset. Only keys supplied by a higher owner replace lower values. Nested groups retain unmodified children and styles. A v2 document can set inherit: false to replace lower ranks completely; higher owner patches still apply. This is what Detach writes. Removing the owner's section restores inheritance.
A global collection owns no card, so it adds no view layer of its own — unless its view borrows one of the home collection's views (cardSources, see Global collections), in which case that view's card is the view layer.
Rows that start open
A list row folds its extra properties behind the › arrow. Set "defaultOpen": true at the top of a row design and its rows arrive already open — the sub-entries checklist and every property showing. It belongs to the whole card (a phone-only or rule override can't change it), and the arrow still folds a row for the rest of your session.
{ "version": 2, "render": "row", "defaultOpen": true }
Next
- Style — every visual key a card can set
- Commands — what a button or an icon can do
- Expressions — the language rules are written in
- Customizing the card — doing all this without writing JSON
Line and open switches
In a list row, show decides where one property appears. line is the row itself; open is the part under it when you open the row. Leave a switch out to keep the normal behaviour.
{ "type": "field", "id": "title", "property": "title", "show": { "line": true, "open": false } }
Sub-entries as rows
"subEntryMode": "rows" draws each sub-entry as a full list row, in its own collection's list design. Add "label": "none" to drop the sub-entries header:
{ "type": "field", "id": "items", "property": "items", "label": "none", "subEntryMode": "rows", "show": { "line": false, "open": true } }
A fixed list — a routine's items you tick but never add to from the list — can drop its + Add with "subEntryHideAdd": true. You can still add children on the entry's own page.
More than one line in a list row
A list row is one line until you ask for more. Any property can use more lines with maxLines (24 means "all"), start its own line with newLine, and the whole row can wrap when full:
{
"version": 2,
"render": "row",
"style": { "flexWrap": "wrap", "alignItems": "start" },
"elements": [
{ "type": "field", "id": "title", "property": "title", "style": { "maxLines": 24 } },
{ "type": "field", "id": "tags", "property": "tags", "style": { "newLine": true } }
]
}