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.
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.
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