Contexts
Real components rarely render in a vacuum — they read a theme, a locale, a feature flag, a current user. A context is windo's way of supplying that surrounding world: a named bundle of live values, the UI to edit them, and an optional React provider to wrap the components that need it. Declare it once in your config and every windo can read from it; opt a windo in and the provider mounts around it.
What a context is for
A context answers a single question: what does this component need from outside itself? That splits cleanly into two jobs, and a context can do either or both.
- Ambient values — light or dark,
enorfr, a flag that's on or off. These are values your component (or your other windo functions) read to decide how to render. They come with editable controls in the chrome, so you can flip them live. - A provider — when the component reaches for a value through React context (
useTheme(), an i18n provider, a query client), windo needs to actually mount that provider in the iframe. The context carries that wrapper, and components opt into it.
You define a context with defineContext, register it under a name in your config, and from then on it's part of the environment every windo runs inside.
Defining a context
defineContext takes one object. Every field is optional — what you supply decides which of the two jobs the context does.
import { defineContext } from '@westopp/windo' import { ThemeProvider } from './app/ThemeProvider' const theme = defineContext({ label: 'Theme', description: 'Colour scheme the component renders under.', controls: { mode: { type: 'enum', options: ['light', 'dark'], default: 'light' }, density: { type: 'enum', options: ['cosy', 'compact'], default: 'cosy' }, }, provider: ({ children, values }) => ( <ThemeProvider mode={values.mode} density={values.density}> {children} </ThemeProvider> ), resolve: (values) => ({ ...values, isDark: values.mode === 'dark' }), })
Reading the fields in order:
labelanddescriptionname the context in the chrome's Context tab —labelis the heading,descriptionthe supporting line. Both are optional; the registration key is used whenlabelis absent.controlsis a map of control key to spec. Each spec declares atype(enum,boolean,string, ornumber), adefault, and the metadata to render its toggle —optionsfor anenum,min/max/stepfor anumber, an optionallabel. The control map's defaults are what the context starts with; the Context tab edits them from there.provideris a React component windo mounts inside the iframe. It receiveschildren(the component being previewed), the livevaluesfrom the controls, and the renderctx. Use it to wrap the component in whatever real provider it expects.resolveshapes what consumers actually read. By default a context exposes its raw control values;resolvelets you derive something richer — combine values, compute a flag, return a whole client object — from(values, ctx). Its return is what lands onctx.contexts[name].
Registering contexts
A context isn't live until your config knows about it. The contexts map in defineWindoConfig is the registry — each key is the name the context is addressed by everywhere else: in a windo's uses, in ctx.contexts, and as the section heading in the chrome when no label is set.
import { defineContext, defineWindoConfig } from '@westopp/windo' const result = defineWindoConfig({ title: 'Acme UI', groups: [ { name: 'Actions', slug: 'actions' }, { name: 'Data display', slug: 'data-display' }, ], contexts: { theme: defineContext({ label: 'Theme', controls: { mode: { type: 'enum', options: ['light', 'dark'], default: 'light' } }, provider: ({ children, values }) => <ThemeProvider mode={values.mode}>{children}</ThemeProvider>, }), locale: defineContext({ label: 'Locale', controls: { lang: { type: 'enum', options: ['en', 'fr', 'de'], default: 'en' } }, }), }, }) export const windo = result.windo export const config = result.config
Here theme carries a provider and locale is ambient-only. Both appear in the Context tab the moment they're registered — you don't opt a windo in to see a context's controls, only to mount its provider.
Ambient values vs. opting in
This is the distinction that trips people up, so it's worth stating plainly. Registering a context makes its values ambient — every windo can read them off ctx.contexts, no opt-in required. What requires opting in is the context's provider: windo only mounts a wrapper around windos that explicitly ask for it.
A component reads an ambient value through ctx.contexts[name]:
export default windo<BannerProps>(() => ({ title: 'Banner', group: 'data-display', defaultProps: { message: 'Heads up' }, // No `uses` — but the locale values are still readable. component: (props, ctx) => { const { lang } = ctx.contexts.locale as { lang: string } return <Banner {...props} lang={lang} /> }, }))
A component opts into a provider with the uses field — a list of context names. windo mounts each named context's provider around this windo (and only this windo), in the order listed.
export default windo<CardProps>(() => ({ title: 'Card', group: 'data-display', // The Theme provider wraps this windo, so `useTheme()` inside Card works. uses: ['theme'], defaultProps: { title: 'Atlas redesign' }, component: (props) => <Card {...props} />, }))
Editing context values live
Each registered context that contributes controls gets a section in the chrome's Context tab. The controls render from the metadata you gave each spec — an enum becomes a select of its options, a boolean a switch, a number a stepper bounded by min / max / step, a string a text field — and they start at each control's default.
Editing a control pushes the new value into the iframe. Both surfaces update at once: the provider re-renders with the fresh values, and ctx.contexts[name] reflects the new resolve output. So flipping theme.mode from light to dark re-runs the ThemeProvider and updates anything reading ctx.contexts.theme.isDark — no reload, no remount of your component tree beyond the provider.
Wrapping a single windo
Contexts are the shared, reusable layer. Sometimes one windo needs a wrapper that nothing else does — a router stub for a link component, a form store for a single field demo, a mock query client scoped to one example. Reaching for a full context is overkill.
The per-windo providers field is the local escape hatch: a single React component that wraps just this windo, in addition to anything uses brings in. It receives children and the render ctx, so it can read live state and environment.
import { MemoryRouter } from 'react-router-dom' export default windo<NavLinkProps>(() => ({ title: 'Nav link', group: 'navigation', // Shared theme provider for everyone… uses: ['theme'], // …plus a router that only this windo needs. providers: ({ children }) => <MemoryRouter>{children}</MemoryRouter>, defaultProps: { to: '/projects', label: 'Projects' }, component: (props) => <NavLink {...props} />, }))
providers and uses compose: the uses providers mount first, then the local providers wraps inside them, then your component. Use a registered context when more than one windo shares a wrapper; reach for providers when the wrapper belongs to exactly one example.
Where next
- Variants — pre-baked prop sets that render in the gallery and apply on click.
- State & actions — the per-windo state machine and the render-time rule that governs
providerstoo.