Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
The CodeControllerContext provides a React context for managing controlled code state in interactive code editing and demo scenarios. It enables real-time code editing with syntax highlighting and live component previews.
CodeHighlighter and demo componentsCodeControllerContext is designed for interactive code editing scenarios where you need:
Use it when building interactive documentation, code playgrounds, tutorials, or any scenario where users need to edit and see live updates to source code.
// Welcome to the live code editor!
function greet(name) {
return `Hello, ${name}!`;
}
import * as React from 'react';
import { CodeHighlighter } from '@mui/internal-docs-infra/CodeHighlighter';
import { createParseSource } from '@mui/internal-docs-infra/pipeline/parseSource';
import { CodeController } from './CodeController';
import { CodeEditorContent } from './CodeEditorContent';
const initialCode = {
Default: {
url: 'file://live-example.js',
fileName: 'live-example.js',
source: `// Welcome to the live code editor!
function greet(name) {
return \`Hello, \${name}!\`;
}
`,
},
};
export function CodeEditor() {
return (
<CodeController url={initialCode.Default.url}>
<CodeHighlighter
url={initialCode.Default.url}
Content={CodeEditorContent}
code={initialCode}
controlled
sourceParser={createParseSource()}
/>
</CodeController>
);
}
This block starts read-only via initialDisabled; the Edit button flips it editable
through the editable / setEditable pair useCode returns (see
Turning editing on and off).
// Read-only until you click Edit.
function greet(name) {
return `Hello, ${name}!`;
}
import * as React from 'react';
import { CodeHighlighter } from '@mui/internal-docs-infra/CodeHighlighter';
import { createParseSource } from '@mui/internal-docs-infra/pipeline/parseSource';
import { CodeController } from './CodeController';
import { EditableToggleContent } from './EditableToggleContent';
const initialCode = {
Default: {
url: 'file://read-only-example.js',
fileName: 'read-only-example.js',
source: `// Read-only until you click Edit.
function greet(name) {
return \`Hello, \${name}!\`;
}
`,
},
};
export function EditableToggle() {
return (
// `initialDisabled` starts the block read-only; the Content's "Edit" button turns
// editing on with the `setEditable` that `useCode` returns.
<CodeController url={initialCode.Default.url}>
<CodeHighlighter
url={initialCode.Default.url}
Content={EditableToggleContent}
code={initialCode}
controlled
initialDisabled
sourceParser={createParseSource()}
/>
</CodeController>
);
}
import React from 'react';
export default function App() {
return (
<div className="container">
<h1>Multi-File Demo</h1>
<p>Edit both the component and CSS!</p>
</div>
);
}import * as React from 'react';
import { CodeHighlighter } from '@mui/internal-docs-infra/CodeHighlighter';
import { createParseSource } from '@mui/internal-docs-infra/pipeline/parseSource';
import { CodeController } from './CodeController';
import { MultiFileContent } from './MultiFileContent';
const initialCode = {
Default: {
url: 'file:///App.tsx',
fileName: 'App.tsx',
source: `import React from 'react';
export default function App() {
return (
<div className="container">
<h1>Multi-File Demo</h1>
<p>Edit both the component and CSS!</p>
</div>
);
}`,
extraFiles: {
'styles.css': {
source: `.container {
padding: 20px;
background: #f2eff3;
border-radius: 8px;
}
h1 {
color: #84828e;
margin-bottom: 10px;
}
p {
color: #65636d;
font-size: 14px;
}`,
},
},
},
};
export function MultiFileEditor() {
return (
<CodeController url={initialCode.Default.url}>
<CodeHighlighter
url={initialCode.Default.url}
Content={MultiFileContent}
code={initialCode}
controlled
sourceParser={createParseSource()}
/>
</CodeController>
);
}
The Code Controller provides a simple React context for managing controlled code state. It works with two main patterns:
For interactive code editing with syntax highlighting:
'use client';
import * as React from 'react';
import { CodeProvider } from '@mui/internal-docs-infra/CodeProvider';
import { CodeControllerContext } from '@mui/internal-docs-infra/CodeControllerContext';
import type { ControlledCode } from '@mui/internal-docs-infra/CodeHighlighter/types';
import { CodeHighlighter } from '@mui/internal-docs-infra/CodeHighlighter';
// Simple controller implementation
function CodeController({ children }: { children: React.ReactNode }) {
const [code, setCode] = React.useState<ControlledCode | undefined>(undefined);
const contextValue = React.useMemo(() => ({ code, setCode }), [code, setCode]);
return (
<CodeControllerContext.Provider value={contextValue}>{children}</CodeControllerContext.Provider>
);
}
// Usage
function CodeEditor() {
const initialCode = {
Default: {
url: 'file://example.js',
fileName: 'example.js',
source: `function greet(name) {
return \`Hello, \${name}!\`;
}`,
},
};
return (
<CodeProvider>
<CodeController>
<CodeHighlighter
url={initialCode.Default.url}
code={initialCode}
controlled
Content={YourEditorContent}
/>
</CodeController>
</CodeProvider>
);
}
For live component previews with editable source — where edited code is transpiled and rendered as a running component — use the useDemoController hook. It owns the controlled code, runs each variant off the main thread, and returns the controller value directly:
const value = useDemoController();
See useDemoController for the full live-editing setup, including the ClientProvider required to execute code in the browser.
The useCode hook automatically integrates with the controller whenever a CodeControllerContext provider is in scope. Set the controlled prop on CodeHighlighter so the block renders through the client path that keeps the controller in sync:
'use client';
import { useCode } from '@mui/internal-docs-infra/useCode';
function EditorContent(props) {
const code = useCode(props);
return (
<div>
<h4>{code.selectedFileName}</h4>
{code.selectedFile}
</div>
);
}
The useDemo hook provides additional functionality for live component demos.
Editing is handled automatically by the Pre component — just render demo.selectedFile:
'use client';
import { useDemo } from '@mui/internal-docs-infra/useDemo';
function DemoContent(props) {
const demo = useDemo(props);
return (
<div>
{/* Live component preview */}
<div>{demo.component}</div>
{/* Editable source code — editing is handled automatically */}
<div>
<select
value={demo.selectedVariant}
onChange={(event) => demo.selectVariant(event.target.value)}
>
{demo.variants.map((variant) => (
<option key={variant} value={variant}>
{variant}
</option>
))}
</select>
{demo.selectedFile}
</div>
</div>
);
}
When setSource is wired up (automatic with useDemo + a controller), the <Pre> rendered by demo.selectedFile adds a keyboard focus trap so that Tab through the page doesn't get caught by contentEditable's indent behavior. See Editable Code Focus Trap below for the interaction details and the CSS hooks you can style.
The CodeController provides a simple state management pattern:
code: undefined)CodeHighlighter loads initial code from file or propsPre component, setSource updates the controller// Flow: User Edit (in Pre) → setSource → controller setCode → Context Update → Re-render
function EditorContent(props) {
const code = useCode(props); // Connects to controller context
// code.selectedFile is a <Pre> component that handles editing automatically
return <div>{code.selectedFile}</div>;
}
Beyond code / setCode, the controller context carries two more channels:
selection / setSelection hold the UI selection — { variant, fileName?, transformKey? } — at the controller level. Every block under the provider reads the same selection, so switching the variant (or file, or transform) in one block switches all of them, and external UI can drive every block at once by calling setSelection:
'use client';
import * as React from 'react';
import {
CodeControllerContext,
type Selection,
} from '@mui/internal-docs-infra/CodeControllerContext';
import type { ControlledCode } from '@mui/internal-docs-infra/CodeHighlighter/types';
function SyncedController({ children }: { children: React.ReactNode }) {
const [code, setCode] = React.useState<ControlledCode | undefined>(undefined);
const [selection, setSelection] = React.useState<Selection>({ variant: 'CssModules' });
const contextValue = React.useMemo(
() => ({ code, setCode, selection, setSelection }),
[code, setCode, selection, setSelection],
);
return (
<CodeControllerContext.Provider value={contextValue}>{children}</CodeControllerContext.Provider>
);
}
Selection works with build-time and client-side loading. With server loading, the selection cannot apply to fallback loading — pass it directly to CodeHighlighter inside the server component instead.
onActivate fires once when a block in the controller's scope first activates for editing — immediately under editActivation: 'eager', or on the first hover/focus/click under 'interaction'. Use it to fetch the editable source or light up surrounding UI at exactly the moment editing begins; CodeHighlighter separately warms its own live-editing dependencies (engine, grammars, worker) at the same moment, so leave it unset if the defaults are all you need.
The createDemo and createLiveDemo factories you implement in your repo (via abstractCreateDemo) automatically handle controller integration:
// demos/my-demo/index.ts
import { createDemo } from '../createDemo';
import { MyComponent } from './MyComponent';
export const DemoMyComponent = createDemo(import.meta.url, MyComponent, {
name: 'My Component Demo',
slug: 'my-component-demo',
});
When used with a controller, the demo automatically:
useCode or useDemoLive demos execute edited code in the browser, so they need a ClientProvider to hoist their component's dependencies (and any external libraries) into the client bundle. That setup — createDemoClient, createLiveDemo, and the ClientProvider wiring — lives with the useDemoController hook, which drives the live previews. A plain code-editor controller needs none of it.
Whenever a controller supplies a setSource handler (the default for useDemo + CodeControllerContext), the <Pre> component activates a small accessibility-focused interaction layer to keep the page's tab order predictable while still allowing rich in-place editing.
contentEditable reassigns the meaning of Tab from "move focus to the next element" to "insert indentation". For a user navigating the page with the keyboard, this turns an editable code block into a focus trap: once they tab into it, they can't tab back out.
The focus trap addresses this by introducing a deliberate "prompt → engage" two-step interaction for keyboard users, while leaving mouse users completely unaffected.
<pre contenteditable> is wrapped in a <div role="group" tabindex="0"> that becomes the only keyboard-tab stop for the block. The inner <pre> is given tabindex="-1" so sequential Tab navigation skips it.:focus-visible), it sets data-editable-prompt="" on itself and reveals an overlay child instructing the user to press Enter to start editing.<pre>, engaging contentEditable behavior — including Tab inserting indentation rather than moving focus.<pre>, so pointer users engage editing immediately without ever seeing the prompt.The wrapper is only rendered when setSource is provided, so non-editable code blocks continue to render a bare <pre> and are unaffected.
The wrapper and overlay ship with sensible default behavior — the overlay is rendered with the hidden attribute by default and <Pre> toggles it as the prompt is shown/hidden, so the prompt works correctly without any consumer CSS. Stable class names and a data attribute let you override that default to add visual styling and animations:
| Selector | Description |
|---|---|
.editable-code-wrapper | The outer focusable wrapper. Use position: relative to anchor the overlay. |
.editable-code-wrapper[data-editable-prompt] | Present only while the wrapper holds keyboard focus and the prompt is shown. |
.editable-code-wrapper:focus-visible | Style the wrapper's keyboard focus ring. |
.editable-code-overlay | The "Press Enter to start editing" prompt. Carries [hidden] while the prompt is hidden — override below for animation. |
.editable-code-overlay kbd | The keycap for Enter inside the prompt. |
.code :global(.editable-code-wrapper) {
position: relative;
display: block;
}
.code :global(.editable-code-wrapper):focus-visible {
outline: 2px solid #6f5bff;
outline-offset: -2px;
border-radius: 8px;
}
/* Override the default `[hidden]` so the overlay stays in the layout
while the prompt is hidden; `visibility` (driven by
`data-editable-prompt`) handles the actual show/hide and is animatable. */
.code :global(.editable-code-wrapper) :global(.editable-code-overlay[hidden]) {
display: block;
}
.code :global(.editable-code-wrapper) :global(.editable-code-overlay) {
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
padding: 4px 10px;
border-radius: 6px;
background: rgba(28, 24, 48, 0.92);
color: #fff;
font-size: 12px;
pointer-events: none;
z-index: 1;
visibility: hidden;
}
.code :global(.editable-code-wrapper)[data-editable-prompt] :global(.editable-code-overlay) {
visibility: visible;
}
.code :global(.editable-code-wrapper) :global(.editable-code-overlay) kbd {
padding: 1px 6px;
margin: 0 2px;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
font-size: 11px;
}For performance benchmarks of the CodeControllerContext component, see the Benchmarking Code Controller Context page.
CodeProvider - Always wrap CodeController with CodeProvider for client-side highlightinguseCode and useDemo hooks rather than manual context accessCodeHighlighter flowcontrolled={true} on CodeHighlighter componentsA code-controller component — the counterpart to DemoContent. It wraps children
in a CodeControllerContext provider, typically sourcing the value from a hook
such as useDemoController. Parameterize with T for the controller’s own props
(e.g. CodeController<UseDemoControllerOptions>).
type CodeController = React.ComponentType<CodeControllerProps<{}>>Context for controlling the code shown within the CodeHighlighter component.
To benefit from server or build-time rendering, the initial code should not be provided
to the controller context. It’s recommended to only set code after the first setCode
event fires.
type CodeControllerContext = {
/**
* Controls the code shown within the code highlighter. Unlike the CodeHighlighter component,
* code is always a string to simplify use. It will be highlighted when it is passed as `code`.
* This behavior depends on client-side highlighting and the CodeProvider component.
*/
code?: ControlledCode | null;
/**
* Controls the state for displaying the given code. This works with build-time and client-side
* loading. If using server loading, the selection won't work for fallback loading and would
* have to be passed directly into the CodeHighlighter component within a server component.
*/
selection?: Selection;
/**
* Setter function for updating the code. When provided in the context, this function will be
* called when the user interacts with the code highlighting. It's recommended to only set `code`
* after the first `setCode` event fires to benefit from server or build-time rendering.
*/
setCode?: React.Dispatch<React.SetStateAction<ControlledCode | null>>;
/**
* Setter function for updating the selection state. When provided in the context, this function
* will be called when the user interacts with the code highlighting interface.
*/
setSelection?: React.Dispatch<React.SetStateAction<Selection>>;
/**
* Allows overriding the preview components shown within the CodeHighlighter.
* It's recommended to keep this value undefined until there are any changes made to a
* component's code and passed as `code`. Each variant has a given component,
* e.g. `{ variantA: {}, variantB: {} }`.
*/
components?: Record<string, React.ReactNode>;
/**
* Current runtime error message per variant key (or `null` when the variant
* rendered cleanly), as reported by the preview components. Demos surface the
* selected variant's error via `useDemo().error`.
*/
errors?: Record<string, string | null>;
/**
* Additional source enhancers to apply to parsed HAST sources.
* These are merged with enhancers from CodeProvider and useCode opts.
*/
sourceEnhancers?: ((
root: { data?: unknown | undefined },
comments: {} | undefined,
fileName: string,
) => { data?: unknown | undefined } | Promise)[];
/**
* Called once when a block in this controller's scope first activates for
* editing — immediately for `editActivation: 'eager'`, or on first engagement
* (hover / focus / click) for `'interaction'`. Lets the host react to "editing
* has begun" (e.g. fetch the editable source, light up UI, or preload its live
* runtime). `deps` reports which file kinds the block spans — `js` when it has any
* JS/TS/JSX/TSX/MJS file, `css` when it has any CSS file — so the host can warm only
* the engine chunks it will need. `CodeHighlighter` separately warms its own
* live-editing dependencies (engine, grammars, worker) at the same moment, so a host
* that only wants the default behavior can leave this unset.
*/
onActivate?: (deps: { js: boolean; css: boolean }) => void;
}Props a code-controller component receives: children to wrap and the optional
url identifying the demo, plus any custom props T (typically a specific
controller’s options). Mirrors ContentProps — a small base extended by T.
type CodeControllerProps<T extends {} = {}> = { children: React.ReactNode; url?: string } & Ttype Selection = { variant: string; fileName?: string; transformKey?: string }CodeProvider: Required for client-side code highlighting and processing'use client')CodeHighlighter components must have controlled={true}createDemoClient to create one)useCode Hook - Integrates CodeHighlighter with controller stateuseDemo Hook - Extends useCode with live component preview capabilities