Today Canvas Kit

Visual container contract (chrome)

The host paints the frosted-translucent frame; the widget renders content-only. manifest.chrome opts a widget out for hello-tier self-chromed cases.

The host paints the tile chrome around your widget — the rounded surface, the border, the soft shadow, the translucent blurred background. The widget renders content-only inside it. This page pins:

  1. The frosted frame the host provides (light + dark recipe).
  2. The widget-side obligation to default to a transparent root.
  3. The manifest.chrome: 'host' | 'opaque' opt-out for hello-tier widgets that legitimately own their own surface.

Sibling pages: container contract (the typed size/theme offer the host makes), render-mode contract (where the chrome lives — inline shadow root vs iframe), feed-batch contract (per-batch grouping above the chrome layer).

What the host provides

A semi-transparent, blurred (frosted-glass) rounded surface with a soft border and a soft shadow — sized to the slot (any of manifest.sizes), themed by ContainerParams.theme.

Light recipe

rounded-3xl
border border-white/40
bg-white/60
backdrop-blur-sm
shadow-[0_2px_10px_rgba(171,172,175,0.12),inset_0_1px_1px_rgba(255,255,255,0.25)]
  • Translucent fill (bg-white/60) — the user sees the stage / page background through the frame, blurred. On a varied stage (sky, dark, neutral) the widget integrates rather than sits-on-top.
  • backdrop-blur-sm — the frosted-glass effect. Stage content behind the frame is blurred so widget content sits on a quiet surface without losing the stage's colour cast.
  • border border-white/40 — a low-contrast edge that reads as a highlight on the translucent surface rather than a hard outline.
  • Compound shadow — an outer soft drop plus an inset 1px white highlight, giving a subtle glass-edge bevel.
  • rounded-3xl — the host-owned corner radius. Widgets put no radius on the root.

Dark recipe

Same shape, dark-inverted:

  • Translucent fill on the dark side (e.g. bg-black/40 or bg-neutral-950/55 — exact alpha is a design call, high enough that text is legible, low enough that the stage shows through).
  • backdrop-blur-sm unchanged.
  • Border border-white/10 — a subtle highlight edge on dark, not a contrast outline.
  • Compound shadow with darker outer + a smaller white inset highlight (rgba(255,255,255,0.04) order-of-magnitude).
  • rounded-3xl unchanged.

Sizing

The frame sizes to the slot (size-full). manifest.sizes (per the size contract) is unaffected — the frame fills whatever cell the host's grid / feed allocates. Both grid widgets (1x14x4) and feed cards (fill-auto) sit in the same frame; only the slot's outer dimensions differ.

Roles

RoleWhoMechanism
Provide the frameHost<WidgetMount> (post-Task #57) — light/dark per ContainerParams.theme, sized per ContainerParams.size.
Consume the frameWidgetRoot element renders transparent by default; content layouts inside.
Tint on top (optional)WidgetA low-alpha tint or gradient layered above the host's translucent bg — alpha cap ~15%, theme-aware.
Opt out (trivial only)Widgetmanifest.chrome: 'opaque' — self-draw a card; intended for hello-tier demo / playground widgets.

The contract holds when either (a) the host renders a frosted frame around the slot AND the widget root is transparent or low-alpha tinted, or (b) the widget declares manifest.chrome: 'opaque' and the host omits its frame for that mount.

Tints — what "subtle" means

Tints are a colour-accent technique that preserves the host frame. The cap is a guideline; the principle is "the frosted surface remains the dominant visual register and the stage colour still shows through."

// ❌ opaque gradient — hides the frosted frame, reads as pasted-on
<article className='size-full bg-gradient-to-br from-sky-100 via-sky-50 to-amber-50 p-4'>

// ✅ low-alpha tint — host frame remains the surface
<article className='size-full bg-gradient-to-br from-sky-500/10 via-transparent to-amber-500/8 p-4'>

Alpha ≤ ~15% is the allowed tint range. An opaque root background requires the manifest.chrome: 'opaque' opt-out.

The hello-tier opt-out — manifest.chrome

// On WidgetManifest (additive; default 'host').
readonly chrome?: 'host' | 'opaque'
  • 'host' (default) — host provides the frame; widget renders transparent or subtle-tint.
  • 'opaque' — host omits its frame for this mount; widget self-draws fully. For hello-tier / demo / standalone widgets that legitimately want their own surface (a welcome banner, a debug card).

The opt-out is explicit in the manifest so the audit can tell intent from violation, and so the host decides whether to wrap the mount in the frame at mount time based on manifest.chrome.

Where the contract lives

The frosted frame is the visual extension of <WidgetMount> — the host's slot chrome around the per-instance React root inside the shadow DOM. The shared reset stylesheet adopted into every shadow root defaults the widget root to transparent; manifest.chrome === 'opaque' gates a per-widget override. After Task #57 the contract is enforced by construction: a widget root that paints opaque without declaring 'opaque' paints over the host's frame, which is visibly wrong on a non-default stage background and caught at QA.

Pre-existing widget guidance

Two related rules are pinned on the widget guidelines page (audience: widget authors):

  • Default transparent. The host owns the surface. Your widget renders inside a host-provided frosted frame; your content sits on top of that frame — no opaque background of your own.
  • Trivial widgets — manifest.chrome: 'opaque'. A demo / hello-tier widget that legitimately wants to own its full surface declares it explicitly.

Both lines flow from this contract.

On this page