MUI Docs Infra

Warning

This is an internal project, and is not intended for public use. No support or stability guarantees are provided.

Code Controller Context

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.

Features

  • Interactive code editing - Edit source code with real-time updates
  • Live component previews - See component changes rendered instantly
  • Context-based state - Shared state across multiple components
  • Simple integration - Works with existing CodeHighlighter and demo components

When to Use

CodeControllerContext is designed for interactive code editing scenarios where you need:

  • Real-time code editing with syntax highlighting
  • Live component previews that update as code changes
  • Shared state between multiple code-related components

Use it when building interactive documentation, code playgrounds, tutorials, or any scenario where users need to edit and see live updates to source code.

Examples

Live Code Editor

live-example.js
// 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>
  );
}

Editable Toggle

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-example.js
// 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>
  );
}

Multi-File Editor

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>
  );
}

Basic Usage

The Code Controller provides a simple React context for managing controlled code state. It works with two main patterns:

Pattern 1: Code Editor

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>
  );
}

Pattern 2: Live Demo Controller

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.

Working with useCode Hook

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>
  );
}

Working with useDemo Hook

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.

Data Flow

The CodeController provides a simple state management pattern:

  1. Initial State: Controller starts with empty state (code: undefined)
  2. Code Loading: CodeHighlighter loads initial code from file or props
  3. User Interaction: When users edit code in the Pre component, setSource updates the controller
  4. State Update: Controller state updates and all connected components re-render
  5. Live Preview: For demo controllers, components are regenerated from updated code
// 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>;
}

Selection Sync and Activation

Beyond code / setCode, the controller context carries two more channels:

Synchronized Selection

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.

Reacting to Activation

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.

Integration with createDemo

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:

  • Loads source code from the filesystem
  • Provides editing capabilities through useCode or useDemo
  • Updates the controller state when code changes
  • Re-renders connected components

Client Dependencies for Live Demos

Live 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.

Editable Code Focus Trap

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.

Why It Exists

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.

Interaction Model

  • The <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.
  • When the wrapper receives keyboard focus (matched via :focus-visible), it sets data-editable-prompt="" on itself and reveals an overlay child instructing the user to press Enter to start editing.
  • Pressing Enter on the wrapper moves focus into the <pre>, engaging contentEditable behavior — including Tab inserting indentation rather than moving focus.
  • Pressing Escape while editing returns focus to the wrapper, restoring normal tab navigation through the page. The overlay reappears so the user knows they can re-engage with Enter.
  • Mouse clicks bypass the wrapper and land directly on the <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.

Styling Hooks

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:

SelectorDescription
.editable-code-wrapperThe 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-visibleStyle the wrapper's keyboard focus ring.
.editable-code-overlayThe "Press Enter to start editing" prompt. Carries [hidden] while the prompt is hidden — override below for animation.
.editable-code-overlay kbdThe keycap for Enter inside the prompt.
DemoLiveContent.module.css
.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;
}

Benchmarking

For performance benchmarks of the CodeControllerContext component, see the Benchmarking Code Controller Context page.

Best Practices

  1. Use with CodeProvider - Always wrap CodeController with CodeProvider for client-side highlighting
  2. Keep controllers simple - Controllers should only manage state, not complex logic
  3. Let hooks handle integration - Use useCode and useDemo hooks rather than manual context access
  4. Start with empty state - Let the initial code load through the normal CodeHighlighter flow
  5. Use controlled prop - Set controlled={true} on CodeHighlighter components

Types

CodeController

A 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<{}>>
CodeControllerContext

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;
}
CodeControllerProps

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 } & T
Selection
type Selection = { variant: string; fileName?: string; transformKey?: string }

Requirements

  • CodeProvider: Required for client-side code highlighting and processing
  • Client component: CodeController must be a client component ('use client')
  • Controlled prop: CodeHighlighter components must have controlled={true}
  • ClientProvider: Required for live demos that execute code dynamically (use createDemoClient to create one)