Today Canvas Kit

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 widgetFeed 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, chromeyesyes
Tailwind v4 (utility-first, host CSS singleton)yesyes
Browser storage forbidden (localStorage, …)yesyes
Theme via useTheme() / dark:yesyes
useWidgetCtx, useTheme, useCurrentWidgetSize, useVisibilityavailableavailable
Lifecycle (can mount, unmount, re-mount at any time)yesyes
Outer container is size-full p-4yesyes
No border-radius / border / shadow on the rootyesyes

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 widgetFeed card
manifest.cardType'widget' (default)'feed'
Typical sizesfixed ('1x1', '2x1', '2x2', …)['fill-auto']
Lives ingrid canvasfeed stack
Resizable by useryesno
Draggable to reorderyesno
Doc store (useFieldState, useDoc, ctx.applyPatch)full read/writeno-op (feed cards have no doc store)
Lifetime in the user's viewlong — pinned, persists across reloadsone-shot — push, read, scroll past
Driven byuser interactionagent generation
Widthfrom grid cellfrom container
Heightfrom grid cellintrinsic to content (host measures via ResizeObserver)
Re-renders over timeyes (state changes)no (snapshot at write time)
Migrations / schemaVersion ladderyes — forward-only migrations arrayno — 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

On this page