Browse topics

Cards

6 min read

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

TypeIsCarries
fielda bound cell — it names one of your propertiesproperty, control, display, options, label, style, on
buttona control whose whole meaning is its handlerlabel, icon, style, on
textwords you wrotetext, style, on
iconan emoji, or an icon by nameicon, style, on
imagea picture you attachedmedia, style, on
groupa nestable boxelements, 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.

  • control picks 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.
  • display picks an alternate presentation — how the value reads when you are not editing it. stat for a big number, hero or cover for an image that fills the card, thumb for a small one, trace for a route, value for 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

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 } }
  ]
}
Didn’t find your answer?Email me