windo/API/Render context
API ReferenceObject

Render context

ctx is the live wire into the canvas. Every render-time function a windo declares — component, defaultProps, an action's run and disabled, and the ctx-aware resolvers for props, variants, placement, and code — receives it as its trailing argument, and through it reads the ambient environment (color scheme, viewport, motion, direction, locale), drives component-local and shared state, flips the canvas theme, and reaches the console and opted-in contexts.

Where it comes from

You never construct a WindoRenderContext. The iframe runtime builds one per render from the ambient state the chrome pushes down, then hands the same object to your component(props, ctx), to a function-form defaultProps(ctx), and to each action's run(ctx, active) / disabled(ctx). Read it at render time — never close over it in the factory body, which runs once at definition time before any ctx exists.

TSXbanner.windo.tsx
component: (props, ctx) => (
<Banner
  {...props}
  theme={ctx.colorScheme}
  compact={ctx.viewport.name === 'mobile'}
/>
)

Fields

The state generic threads through: ctx.state and ctx.setState are typed by the windo's State. The rest of the object is fixed.

FieldTypeDescription
colorScheme'light' | 'dark'The active scheme, toggled from the chrome. Branch your component on it instead of reading the host page — the preview is isolated.
setColorScheme(scheme: 'light' | 'dark') => voidSets the canvas color scheme from a component or action. This is the same scheme the chrome topbar toggle drives, so calling it keeps the chrome in sync.
toggleTheme() => voidFlips colorScheme between light and dark — the call-site shorthand for setColorScheme. Wire it to a component's own theme switch to drive the canvas.
viewportWindoViewportThe current frame size: { width, height, name } where name is 'mobile' | 'tablet' | 'desktop'. Use it to drive responsive rendering without a media query.
reducedMotionbooleanTrue when the reduced-motion switch in the Inspector's Context panel is on. Gate animations on it so motion-sensitive previews stay still.
direction'ltr' | 'rtl'Writing direction, set from the Inspector's Context panel and synced to the preview. Mirror layout and icon orientation off this to preview right-to-left rendering.
localestringThe active locale string (e.g. en-US), edited in the same Context panel. Feed it to Intl formatters or your own i18n lookup.
loggerWindoLoggerConsole channel for the preview. logger.log(...args) posts an entry to the chrome's Console tab — the windo runs in an iframe, so this is how output surfaces.
stateStateCurrent component-local state, seeded from the windo's state field and typed by the State generic. Read-only here — mutate through setState.
setState(patch: Partial<State>) => voidMerges a partial patch into the component-local state and re-renders. This is how actions and the component itself advance state.
contextsRecord<string, unknown>Resolved values of the contexts this windo opted into via uses, keyed by context name. Each value is whatever that context resolved to (its resolve return, or its control values by default).
ctxStateRecord<string, unknown>The shared, cross-component state bag, seeded from the config's ctxState field. Unlike state, it persists across windo selection and any component can read it. Editable from the chrome's Shared strip.
setCtxState(patch: Record<string, unknown>) => voidShallow-merges a patch into the shared ctxState and re-renders every consumer. Use it to flip a value (a theme, a language) that other components react to.

colorScheme, setColorScheme, toggleTheme

ctx.colorScheme is the ambient light/dark scheme the chrome's toolbar controls. It arrives read-only on every render, so the usual move is to read it in component and branch. But a component can also drive it: ctx.setColorScheme('dark') and the ctx.toggleTheme() shorthand set the same scheme the topbar toggle controls, and the chrome stays in sync. Wire toggleTheme to a component's own theme switch and the canvas follows along.

TSXtheme-switch.windo.tsx
component: (props, ctx) => (
<ThemeSwitch
  {...props}
  scheme={ctx.colorScheme}
  onToggle={() => ctx.toggleTheme()}
/>
)

viewport, reducedMotion, direction, locale

These four are the rest of the ambient environment. viewport tracks the canvas frame — resize it and the value follows; reducedMotion, direction, and locale are set from the Inspector's Context panel and synced to the preview. They arrive read-only on every render, so the way to respond is to read them in component and branch. Because the preview lives in an isolated iframe, these are the source of truth for the environment; don't reach for the host document's theme or matchMedia.

TSXprice.windo.tsx
component: (props, ctx) => {
const price = new Intl.NumberFormat(ctx.locale, {
  style: 'currency',
  currency: 'USD',
}).format(props.amount)

return (
  <Tag
    dir={ctx.direction}
    animate={!ctx.reducedMotion}
    compact={ctx.viewport.name !== 'desktop'}
  >
    {price}
  </Tag>
)
}

logger

The preview renders inside an iframe, so a bare console.log lands in the iframe's own console, not the chrome. ctx.logger.log(...args) bridges that gap — each call posts a WindoLogEntry to the chrome's Console tab, where you read it alongside the canvas. Reach for it to trace renders, action fires, and prop changes.

TSXselect.windo.tsx
component: (props, ctx) => (
<Select
  {...props}
  onChange={value => {
    ctx.logger.log('selected', value)
    ctx.setState({ value })
  }}
/>
)

state and setState

ctx.state is the windo's component-local state — the same shape you declared in the state field, typed by the State generic. It is read-only on the ctx; to advance it, call ctx.setState(patch), which merges the partial patch and re-renders. This pair is what makes a windo interactive: the component reads ctx.state to render, and both the component and the windo's actions call ctx.setState to move it forward.

TSXtoast.windo.tsx
state: { open: false },
actions: [
{ label: 'Show', run: ctx => ctx.setState({ open: true }) },
{ label: 'Hide', run: ctx => ctx.setState({ open: false }), disabled: ctx => !ctx.state.open },
],
component: (props, ctx) => <Toast {...props} open={ctx.state.open} />,

contexts

When a windo opts into a provider context via uses, the resolved value of each one shows up on ctx.contexts, keyed by context name. The value is whatever that context's resolve returned — or, when it has none, its raw control values. It is typed unknown, so narrow it before use.

TSXcard.windo.tsx
uses: ['theme'],
component: (props, ctx) => {
const theme = ctx.contexts.theme as { accent: string }
return <Card {...props} accent={theme.accent} />
}

ctxState and setCtxState

ctx.state is private to one windo and resets when you select another. ctx.ctxState is the opposite: one shared bag, seeded from the config's ctxState field, that persists across selection and that every component can read and write. Read it on the ctx; patch it with ctx.setCtxState(patch), which shallow-merges and re-renders every consumer. The chrome exposes it too — you can edit it by hand from the Shared strip.

Its reach is a provider. Wrap your opted-in components in a provider that paints from ctx.ctxState, and flipping the value from any component repaints that provider for whichever component is on the canvas — a theme, a language, a feature flag shared across the whole set.

TSXlanguage-toggle.windo.tsx
component: (props, ctx) => (
<button onClick={() => ctx.setCtxState({ language: ctx.ctxState.language === 'en' ? 'fr' : 'en' })}>
  {String(ctx.ctxState.language)}
</button>
)

ctx-aware definition fields

ctx does more than reach component and actions. Several windo() fields accept a ctx => value function in place of a static value — a Ctxual<T> — and the runtime calls it with this same context. placement, props, variants, and state resolve to a value; code gains ctx as a second argument. A resolver must be a pure read of ctx — calling setState, setCtxState, or setColorScheme during resolution no-ops.

When each resolves differs, and it matters:

  • placement and defaultProps re-resolve on every render — they track ctx live, so ctx => ctx.viewport.name === 'mobile' ? 'fill' : 'center' reflows as you resize the frame, and a ctx => props default re-derives each render.
  • props, variants, and code resolve once at selection — a snapshot of ctx taken when the component is picked, not re-run on later env changes.
  • state resolves once at selection too, but against a WindoInitContext — this ctx without state/setState, since it is defining the initial state.
TSXplacement.windo.tsx
placement: ctx => (ctx.viewport.name === 'mobile' ? 'fill' : 'center'),