The config
A config file default-exports defineStudioConfig({ ... }). defineStudioConfig returns its argument; it exists so the object is typed. The runtime exports are defineStudioConfig and adoptStylesheet. The types are StudioConfig, StudioCompose, StudioRuntime, StudioTheme, and, for apps, StudioApp, StudioAppPage, StudioAppProps, StudioAppSurface, StudioPreviewProps, StudioSurfaceProps, PreviewZoom and RenderPdf.
import { createIsomerRuntime } from '@elastic/isomer-runtime';
import { defineStudioConfig } from '@elastic/isomer-studio';
import { previewSlide } from './src/examples/preview_slide';
import { slideDeckFrame, slidesPack, slideStylesheet } from './src/index';
export default defineStudioConfig({
runtime: createIsomerRuntime({
packs: [slidesPack],
frames: { slide: slideDeckFrame },
}),
stylesheet: slideStylesheet,
previewExample: previewSlide,
});
| Field | Required | What it does |
|---|---|---|
runtime |
Yes | The runtime from createIsomerRuntime. Its packs are what the Studio lists, and its surfaces are the previews it offers. |
title |
No | Replaces the header, the page title, and the check report name. Defaults to Isomer Studio. Leave it unset to keep the product name; a runtime with more than one pack shows each pack in the nav. |
stylesheet() |
No | The CSS the React preview needs. The Studio calls it once and adopts the result into each preview's shadow root. |
createReactContext(root) |
No | Returns the render context the pack's renderers expect, inside the React preview's shadow root, after stylesheet is adopted there. Call adoptStylesheet(root, css) for any other CSS. Without it the context is empty, and without it or stylesheet the React preview shows the html surface instead. |
previewExample(node) |
No | The composition one node is shown in on its own, such as a slide around it. The Studio adds the theme and puts several nodes in a view. See previewExample and compose. |
compose(nodes, { theme }) |
No | Wraps example nodes in a composition, for full control. Wins over previewExample. Defaults to { type: 'view', body: nodes, theme }. |
apps |
No | Pages that demonstrate the pack, listed in the nav and on Home. See Apps. |
imageFonts |
No | A module path, relative to the config, whose default export lists the takumi FontLoaders PNGs and PDFs draw text with. See Image fonts. |
images |
No | false turns off PNGs and PDFs: no PNG previews, Decks PDFs or prerendered PNGs, and takumi never loads. See Turning images off. |
featured |
No | Primitive types Home features, in order. Without it Home draws six at random on each load. See Featured primitives. |
Examples are nodes, not compositions. Every place the Studio needs a composition, the config's compose, or its previewExample, builds it: the gallery, the docs pages, the editor's preview, the PNGs a static build prerenders and every example check renders. A pack writes the envelope once and every mode agrees on it.
The default fits a pack whose primitives render inside a plain view. A pack whose frame requires a particular root says how one node is shown with previewExample. The slides frame draws exactly one slideFrame, so the slides previewExample wraps a lone node in a preview slide, and passes a node that is already a slideFrame through unwrapped. compose replaces both when a pack needs to shape several nodes too.
compose receives the theme the Studio is showing, light or dark, and puts it on the composition; with previewExample, the Studio does. That theme is the app's color mode, so previews always match the chrome around them. Light and dark are then different compositions, which is how a static build tells their PNGs apart.
images: false keeps the Studio in the browser. Node never loads @elastic/isomer-image-takumi or imageFonts, build writes no PNGs, PNG previews and PDF actions are hidden, and dev answers /png and /pdf with 404. The snapshot surface's markup still renders, and check --png refuses the config.
Toggling it in dev hot-updates in place, keeping the route and the editor's draft. An app that offers PNGs or PDFs should hide those actions when rasterizePng or renderPdf is absent, and fall back when a surface it is showing leaves surfaces, as Decks does.
Home opens with a strip of featured primitives, each with a link to its Reference page and to the Workbench. featured names them by type, in the order they appear:
export default defineStudioConfig({
runtime,
featured: ['titleSlide', 'slideBulletList', 'slideSplitLayout'],
});
Without featured, Home draws six primitives at random each time it loads, so a pack with no opinion still shows its range. dev, build and check reject a featured entry the runtime has no primitive for, or one listed twice, and name it. A browser hot update with an invalid entry reports it and skips it.
An app is a whole page of the Studio, such as a deck viewer. Each one gets a top-level nav entry beside Gallery, a tile on Home, and a route at #/app/<id>/.
export default defineStudioConfig({
runtime,
apps: [
{
id: 'decks',
title: 'Decks',
description: 'Whole presentations, played one slide at a time.',
load: () => import('./studio/decks_app'),
},
],
});
| Field | Required | What it does |
|---|---|---|
id |
Yes | The route segment, matching /^[a-z][a-z0-9-]*$/ and unique within the config. |
title |
Yes | The nav entry, the header's label while the app is open, and the Home tile's title. |
description |
No | One line under the title on the Home tile and in Home's Loaded panel. |
icon |
No | A PrimitiveIcon, held to the same rules as a primitive's icon by check. Without one, the nav and Home show the title's initial. |
load |
Yes | Imports a module whose default export is the app's page. |
pages |
No | Nav rows under the app's own row. See Pages. |
dev, build and check reject invalid apps and name each one. A hot update with invalid apps keeps the last valid ones and shows why. check counts the apps but never calls load.
An app can list its own pages in the nav, such as one per deck. The nav shows them without loading the app, so a config builds them from data it already imports.
pages: [
{ path: 'isomer', title: 'Isomer', section: 'Decks' },
{ path: 'distillate', title: 'Distillate', section: 'Decks' },
],
| Field | Required | What it does |
|---|---|---|
path |
Yes | Opens #/app/<id>/<path>. One or more non-empty segments, with no leading or trailing /, unique within the app. |
title |
Yes | The row's label. The nav filter matches it. |
section |
No | Pages with the same section are listed under it, in order of first appearance. Pages without one come first. |
A page is current when the app's path equals the page's path or starts with it plus /. The longest match wins, so isomer/3 keeps a page at isomer/3 current over one at isomer. While a page is current, the app's own row is not. The collapsed rail shows only the app's row.
dev, build and check reject invalid pages as they do invalid apps, naming the app and the page's index.
The page is a React component, and the Studio hands it StudioAppProps:
| Prop | What it is |
|---|---|
Preview |
Renders pack output inside a shadow root, isolated from the app's styles and the Studio's. Pass nodes, which go through the config's compose or previewExample at the current theme, or a finished composition. zoom defaults to fit, and maxHeight caps a fitted preview's height. heading={false} leaves out the composition's leading title, as a slide shown on its own wants. enhancements are enhancement ids the surface renders with, such as the slides pack's SLIDE_BUILDS. onCommit(root) runs with the shadow root after each render commits, for a host step such as showSlideBuild. Without createReactContext or stylesheet, it shows the html surface, which gets enhancements but has no shadow root, so onCommit is never called. |
Surface |
Shows a composition on one of surfaces, as the Studio's own previews do: surface picks it, view is render or source, and zoom and heading work as on Preview. png is drawn by the host. |
surfaces |
The { id, label } surfaces Surface can show, in the Studio's order: the runtime's, then png when the host rasterizes. |
renderPdf |
renderPdf(compositions, { title }) resolves to a PDF Blob, a page per composition, drawn as PNGs are. Only dev has one; it is undefined in a static build. |
runtime |
The config's runtime. |
theme |
light or dark, following the Studio's color mode. |
path |
Everything after #/app/<id>/, slashes included, with each segment percent-decoded. |
navigate(path) |
Moves to another path within the app, adding a history entry. Each segment is percent-encoded in the hash, so path reads back the same after a reload. |
Import only types from @elastic/isomer-studio in an app module. The Studio hands the page everything it needs as props.
The page stays mounted when path, the theme or the config changes. When the config changes, the Studio calls the new load and remounts the page only if the module's default export is a different component. Edits to the app module update it in place through React Fast Refresh, as long as the module exports only components. A module that exports anything else re-runs the config instead.
load runs only in the browser. Node imports the config for check and build, but never calls load, so code that must stay off the server belongs behind it.
Apps render under the Studio's own EuiProvider. Use EUI components directly, and nest EuiThemeProvider to change the theme for part of the page. Never mount a second EuiProvider.
EUI's theme and Emotion's cache reach the app only when it imports the same copies the Studio does. When a config with apps resolves @elastic/eui or @emotion/react, it must resolve the Studio's copy, and dev, build and check stop when it does not. A config without apps is not checked. Install the Studio's versions of EUI and its peers beside the config: @elastic/eui, @elastic/eui-theme-borealis, @elastic/datemath, @emotion/react, @emotion/css and moment.
Apps are not inside a shadow root, because EUI's popovers, flyouts, modals and toasts portal to document.body.
| Form | Scope | Notes |
|---|---|---|
Emotion's css |
Scoped | Needs @emotion/react as the JSX import source: add /** @jsxImportSource @emotion/react */ to the file, or set jsxImportSource in a tsconfig.json for the app's folder. |
CSS Modules, *.module.css |
Scoped | TypeScript needs a declaration such as declare module '*.module.css' { const classes: Readonly<Record<string, string>>; export default classes; }. |
Plain .css |
Global | Scope its selectors under the app's root element. url()s to fonts (woff2, woff, ttf, otf, eot) and images (png, jpg, jpeg, gif, webp, avif, svg, ico) resolve under any --base. |
In dev, a stylesheet loads when the app first opens. A static build bundles every stylesheet the config imports into assets/app.css, which loads with the page.
The Studio offers the surfaces runtime.surfaces has. A runtime without a frame has no snapshot surface, so it gets no Snapshot or PNG previews, a static build prerenders no PNGs, and check reports those checks as skipped.
PNGs and PDFs are drawn by takumi in Node, which knows no fonts but its own. imageFonts names a module that default-exports the faces to draw with, as FontLoader[] from @elastic/isomer-image-takumi:
// studio/image_fonts.ts
export { slideFonts as default } from '../src/examples/fonts';
Node imports it the way it imports the config, the first time it draws, and the browser never does, so it may read font files from disk. dev, build and check --png all draw with it. Without it, PNGs fall back to takumi's built-in face and a PDF cannot draw text.
The config runs in two places: in Node, for check and for the dev server's PNGs, and in the browser, bundled into the app.
In Node, check and build import the config with tsx, which compiles TypeScript and JSX as it loads and leaves resolution to Node. The config, its pack, React and the runtime all resolve from the config's own directory, so they come from the pack's node_modules. tsx applies the tsconfig.json nearest the config, so the pack's jsx setting holds in both ES module and CommonJS projects. Node cannot run ESM built for bundlers, which is what Isomer's own packages ship: an import that lands on such a build, a .js file in a package without "type": "module", takes the package's CommonJS build instead.
dev loads the config through Vite's server-side module graph instead, with the same nearest-tsconfig.json JSX settings. Packages in node_modules load once through Node, except the SDK, the runtime and the Studio, whose ESM builds Vite compiles because Node cannot; an edit re-runs only the files that changed and the modules that import them.
--loader none skips both and imports the config with a plain import(). Use it for a JavaScript config, or to bring another loader through NODE_OPTIONS=--import .... Node caches that import, so under dev the browser picks up edits but PNGs keep the config and pack as they were at startup; restart dev to refresh them.
In the browser, build bundles the config with esbuild and dev serves it through Vite. react, react-dom and their subpaths resolve as if imported from the config, so the Studio's own components, Emotion and the pack share one React, and each package's browser build is the one loaded.
Run with npx, the Studio sits in npm's cache rather than the pack's node_modules. Two rules make that work in Node, Vite and esbuild alike:
react,react-dom,@elastic/isomer-sdkandzod, imported from outside the project, resolve from the project, so the runtime and the pack share them.@elastic/isomer-runtimeand@elastic/isomer-studio, when the project does not have them, resolve from the Studio's install. A config can import both without installing either.
The Studio depends on the SDK and runtime at its own version rather than as peers, so a cached npx @elastic/isomer-studio@next upgrades all three together when next moves.
The project is the directory of the package.json nearest the config. In Node the rules cover import and require, which a .ts config in a project that is not "type": "module" loads through.
The Studio, the pack and the runtime must use one copy of React, and it must be React 18, which EUI requires. Before loading the config, the Studio resolves react from the config, stops when it is not 18, then resolves react from the @elastic/isomer-runtime the config sees, and stops when they differ:
The Studio config and @elastic/isomer-runtime resolve different copies of React:
config: /work/pack/node_modules/react
runtime: /work/pack/node_modules/@elastic/isomer-runtime/node_modules/react
Install one React for both, for example by deduplicating your lockfile.