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

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>
      <CodeHighlighter
        url={initialCode.Default.url}
        Content={CodeEditorContent}
        code={initialCode}
        controlled
        sourceParser={createParseSource()}
      />
    </CodeController>
  );
}

Live Demo

Type Whatever You Want Below

CheckboxBasic.tsx
import * as React from 'react';
import { Checkbox } from '@/components/Checkbox';

export default function CheckboxBasic() {
  return (
    <div>
      <Checkbox defaultChecked />
      <p style={{ color: '#CA244D' }}>Type Whatever You Want Below</p>
    </div>
  );
}

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>
      <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 { CodeHighlighter } from '@mui/internal-docs-infra/CodeHighlighter';

// Simple controller implementation
function CodeController({ children }: { children: React.ReactNode }) {
  const [code, setCode] = React.useState();
  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 code:

'use client';

import * as React from 'react';
import { useRunner } from 'react-runner';
import { CodeControllerContext } from '@mui/internal-docs-infra/CodeControllerContext';

function DemoController({ children }: { children: React.ReactNode }) {
  const [code, setCode] = React.useState();

  // Create live component instances from code
  const components = React.useMemo(() => {
    if (!code) return undefined;

    return Object.keys(code).reduce((acc, variant) => {
      const source = code[variant]?.source;
      if (source) {
        acc[variant] = <Runner code={source} />;
      }
      return acc;
    }, {});
  }, [code]);

  const contextValue = React.useMemo(
    () => ({ code, setCode, components }),
    [code, setCode, components],
  );

  return (
    <CodeControllerContext.Provider value={contextValue}>{children}</CodeControllerContext.Provider>
  );
}

Working with useCode Hook

The useCode hook automatically integrates with the CodeController context when the controlled prop is true:

'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={(e) => demo.selectVariant(e.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 → controlledSetCode → 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>;
}

Integration with createDemo

The createDemo and createLiveDemo functions automatically handle controller integration:

// demos/my-demo/index.ts
import { createDemo } from '@mui/internal-docs-infra/functions/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

For live demos that need to run code dynamically (like the Live Demo example), you must provide a ClientProvider to ensure dependencies are properly bundled:

Using createDemoClient

Note

Before you can use createDemoClient in your demos, you must first create this file in your repo using the abstract factory. See the abstractCreateDemoClient docs for details.

Once you've created your own createDemoClient file, you can use it in your demos as shown below:

client.ts
// demos/my-live-demo/client.ts
'use client';

import { createDemoClient } from '../createDemoClient';

const ClientProvider = createDemoClient(import.meta.url);

export default ClientProvider;

Creating a Custom createDemoClient

You can also create your own createDemoClient using the abstract factory:

createDemoClient.ts
'use client';

import { createDemoClientFactory } from '@mui/internal-docs-infra/abstractCreateDemoClient';
import { DemoController } from './DemoController';

/**
* Creates a demo client copying dependencies in the client bundle for live editing.
* @param url Depends on `import.meta.url` to determine the source file location.
* @param meta Additional meta and modules for the demo client.
*/
export const createDemoClient = createDemoClientFactory({
DemoController,
});

Integration with createLiveDemo

Then use the ClientProvider in your demo configuration:

index.ts
// demos/my-live-demo/index.ts
import { createLiveDemo } from '../createLiveDemo';
import ClientProvider from './client';
import MyComponent from './MyComponent';

export const DemoMyLiveComponent = createLiveDemo(import.meta.url, MyComponent, {
name: 'My Live Component',
slug: 'my-live-component',
ClientProvider, // Required for dependency hoisting
});

The ClientProvider ensures that:

  • Component dependencies are hoisted into the client bundle
  • External libraries are available for the useRunner hook through CodeExternalsContext
  • Live code execution has access to all required modules via precomputed externals

This is only needed for demos that render live components from editable code, not for simple code editor scenarios.

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

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;
  /**
   * 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 | undefined>>;
  /**
   * 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>;
  /**
   * 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)[];
}
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)

Purpose

The CodeControllerContext is specifically 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
  • TypeScript/JavaScript transformation toggle

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