Live widget vs feed card
Two business-level widget styles share the same architecture, SDK, and ABI. The difference is convention, not capability.
TCK ships one widget shape — a .tckb bundle that exports a manifest and a default React component. Hosts speak one wire contract to all of them. But two business-level use-cases are served on top of that one architecture, and the conventions around each are worth knowing before authoring, because they shape the mental model.
Same architecture
| Live widget | Feed card | |
|---|---|---|
| Bundle format | .tckb | .tckb |
| SDK | @todayai-labs/tck | @todayai-labs/tck |
| Mount path | <WidgetMount mode='inline'> (Shadow DOM) | same |
| Host-provided React, importmap, theme, chrome | yes | yes |
| Tailwind v4 (utility-first, host CSS singleton) | yes | yes |
Browser storage forbidden (localStorage, …) | yes | yes |
Theme via useTheme() / dark: | yes | yes |
useWidgetCtx, useTheme, useCurrentWidgetSize, useVisibility | available | available |
| Lifecycle (can mount, unmount, re-mount at any time) | yes | yes |
Outer container is size-full p-4 | yes | yes |
No border-radius / border / shadow on the root | yes | yes |
The host can't tell a live widget from a feed card except by reading manifest.cardType. Everything else is the same code path.
Different conventions
| Live widget | Feed card | |
|---|---|---|
manifest.cardType | 'widget' (default) | 'feed' |
Typical sizes | fixed ('1x1', '2x1', '2x2', …) | ['fill-auto'] |
| Lives in | grid canvas | feed stack |
| Resizable by user | yes | no |
| Draggable to reorder | yes | no |
Doc store (useFieldState, useDoc, ctx.applyPatch) | full read/write | no-op (feed cards have no doc store) |
| Lifetime in the user's view | long — pinned, persists across reloads | one-shot — push, read, scroll past |
| Driven by | user interaction | agent generation |
| Width | from grid cell | from container |
| Height | from grid cell | intrinsic to content (host measures via ResizeObserver) |
| Re-renders over time | yes (state changes) | no (snapshot at write time) |
Migrations / schemaVersion ladder | yes — forward-only migrations array | no — next agent push replaces the card |
Notes on the table
fill-auto is a convention, not a constraint
A widget can declare sizes: ['fill-auto'] with cardType: 'widget', and cardType: 'feed' is technically free to declare other sizes — the SDK validators accept both. But every host built so far routes cardType: 'feed' to the feed stack and cardType: 'widget' to the grid; the two regions are exclusive. By convention fill-auto lives in the feed and fixed sizes ('1x1', '2x1', …) live in the grid, and the pairing falls out of where the widget gets mounted. See documents/container-contract.md §3.3 for the size-contract math.
Feed cards are time-snapshots — by discipline, not by enforcement
Nothing at the runtime layer prevents a feed card from polling, animating, or computing new Date() at render. useState, setInterval, setTimeout, fetch all work in a feed card the same way they work in a live widget. But the product contract of the feed is "the agent prepared this card for you at time T; you're reading it now at T + n." Authoring against that contract means baking values in at write time, not reading them at render time.
If a feed card needs to show today's date, write 'May 28', not new Date().toLocaleDateString(). The agent that generated the card knew the date — bake it in. The same logic applies to live counters, polling fetches, animated tickers, anything that says "right now." If you find yourself reaching for the wall clock, the artifact you're authoring is a live widget, not a feed card; the manifest is wrong before the code is.
The implication: useEffect is rare in feed cards. It's usually a sign you're reaching for runtime state a feed card shouldn't carry. The narrow exceptions are mount-time-only effects that don't conceptually update over time — lazy-loading an image, intersection-observing for analytics.
Live widgets persist; feed cards regenerate
A live widget's state lives in the host's doc store across reloads, theme flips, and (within the manifest's schemaVersion ladder) bundle upgrades. A feed card has no doc store — the next time the agent generates the feed, a fresh card with fresh content replaces it. Authoring a feed card with useState is fine for in-card UI (a disclosure toggle, a hover preview), but state will not survive a feed regeneration.
Picking one
- Choose live widget when the user does something to the artifact (click, drag, edit, mark complete) and the result has to stick across reloads.
- Choose feed card when the agent has finished thinking by the time the card paints — the card is the answer, not the question.
If you find yourself wanting interactivity in a feed card or read-only content in a live widget, the manifest is wrong before the code is.
See also
- Building widgets — the scaffolding + dev loop walkthrough.
- Widget guidelines — the canonical widget-author rulebook (applies to both shapes).
- Widget lifecycle — source → bundle → mounted React tree.
- Agent authoring — how an LLM agent writes either shape against the scaffolder.
- The two scaffolded guidelines files in every
pnpm create @todayai-labs/widgetproject: - Container contract §3.3 —
fill-autoiscardType-gated, not a wildcard. - Platform ABI — the wire-level contract underneath both shapes.