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.
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.
| Field | Type | Description |
|---|---|---|
| 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') => void | Sets 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 | () => void | Flips colorScheme between light and dark — the call-site shorthand for setColorScheme. Wire it to a component's own theme switch to drive the canvas. |
| viewport | WindoViewport | The current frame size: { width, height, name } where name is 'mobile' | 'tablet' | 'desktop'. Use it to drive responsive rendering without a media query. |
| reducedMotion | boolean | True 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. |
| locale | string | The active locale string (e.g. en-US), edited in the same Context panel. Feed it to Intl formatters or your own i18n lookup. |
| logger | WindoLogger | Console 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. |
| state | State | Current 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>) => void | Merges a partial patch into the component-local state and re-renders. This is how actions and the component itself advance state. |
| contexts | Record<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). |
| ctxState | Record<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>) => void | Shallow-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.
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.
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.
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.
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.
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.
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:
placementanddefaultPropsre-resolve on every render — they trackctxlive, soctx => ctx.viewport.name === 'mobile' ? 'fill' : 'center'reflows as you resize the frame, and actx => propsdefault re-derives each render.props,variants, andcoderesolve once at selection — a snapshot ofctxtaken when the component is picked, not re-run on later env changes.stateresolves once at selection too, but against aWindoInitContext— this ctx withoutstate/setState, since it is defining the initial state.
placement: ctx => (ctx.viewport.name === 'mobile' ? 'fill' : 'center'),