MUI Docs Infra

Warning

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

Use Demo

The useDemo hook extends useCode functionality to provide a complete demo rendering solution that combines component previews with code display. It's specifically designed for creating interactive demonstrations where users can see both working React components and their source code.

Like useCode, it implements the Props Context Layering pattern for seamless server-client compatibility.

Overview

Built on top of the useCode hook, useDemo adds demo-specific functionality like name and slug management while inheriting all code management capabilities. This makes it the go-to choice for creating rich interactive demos within the CodeHighlighter ecosystem.

Inheritance: Since useDemo extends useCode, it automatically includes all URL management and file navigation features, including URL hash routing for deep-linking to specific files.

Key Features

  • Complete code management via useCode integration with URL hash routing
  • Component rendering alongside code display
  • Demo identification with automatic name and slug generation from URLs
  • Variant switching for different implementation approaches
  • Transform support for language conversions (TypeScript to JavaScript)
  • File navigation with deep-linking support for multi-file demos

Basic Usage

import { useDemo } from '@mui/internal-docs-infra';

export function DemoContent(props) {
  const demo = useDemo(props, { preClassName: styles.codeBlock });

  return (
    <div className={styles.container} ref={demo.ref}>
      {/* Component Preview Section */}
      <div className={styles.demoSection}>{demo.component}</div>

      {/* Code Section */}
      <div className={styles.codeSection}>
        <div className={styles.code}>{demo.selectedFile}</div>
      </div>
    </div>
  );
}

Advanced Usage

Full Interactive Demo Interface

export function DemoContent(props) {
  const demo = useDemo(props, { preClassName: styles.codeBlock });

  const hasJsTransform = demo.availableTransforms.includes('js');
  const isJsSelected = demo.selectedTransform === 'js';

  const labels = { false: 'TS', true: 'JS' };
  const toggleJs = React.useCallback(
    (checked: boolean) => {
      demo.selectTransform(checked ? 'js' : null);
    },
    [demo],
  );

  const tabs = React.useMemo(
    () => demo.files.map(({ name }) => ({ id: name, name })),
    [demo.files],
  );

  const variants = React.useMemo(
    () =>
      demo.variants.map((variant) => ({
        value: variant,
        label: variantNames[variant] || variant,
      })),
    [demo.variants],
  );

  return (
    <div className={styles.container}>
      {/* Demo Preview */}
      <div className={styles.demoSection}>{demo.component}</div>

      {/* Code Section */}
      <div className={styles.codeSection}>
        <div className={styles.header}>
          <div className={styles.headerContainer}>
            {/* File Tabs */}
            <div className={styles.tabContainer}>
              {demo.files.length > 1 ? (
                <Tabs
                  tabs={tabs}
                  selectedTabId={demo.selectedFileName}
                  onTabSelect={demo.selectFileName}
                />
              ) : (
                <span className={styles.fileName}>{demo.selectedFileName}</span>
              )}
            </div>

            {/* Actions */}
            <div className={styles.headerActions}>
              <CopyButton copy={demo.copy} copyDisabled={demo.copyDisabled} />

              {/* Variant Selector */}
              {demo.variants.length > 1 && (
                <Select
                  items={variants}
                  value={demo.selectedVariant}
                  onValueChange={demo.selectVariant}
                />
              )}

              {/* Transform Toggle */}
              {hasJsTransform && (
                <div className={styles.switchContainer}>
                  <LabeledSwitch
                    checked={isJsSelected}
                    onCheckedChange={toggleJs}
                    labels={labels}
                  />
                </div>
              )}
            </div>
          </div>
        </div>

        {/* Code Display */}
        <div className={styles.code}>{demo.selectedFile}</div>
      </div>
    </div>
  );
}

Editable Demo (Live Editing)

export function DemoLiveContent(props) {
  const preRef = React.useRef<HTMLPreElement | null>(null);
  const demo = useDemo(props, { preClassName: styles.codeBlock, preRef });

  const hasJsTransform = demo.availableTransforms.includes('js');
  const isJsSelected = demo.selectedTransform === 'js';

  const labels = { false: 'TS', true: 'JS' };
  const toggleJs = React.useCallback(
    (checked: boolean) => {
      demo.selectTransform(checked ? 'js' : null);
    },
    [demo],
  );

  const tabs = React.useMemo(
    () => demo.files.map(({ name }) => ({ id: name, name })),
    [demo.files],
  );

  const variants = React.useMemo(
    () =>
      demo.variants.map((variant) => ({
        value: variant,
        label: variantNames[variant] || variant,
      })),
    [demo.variants],
  );

  // Set up editable functionality
  const onChange = React.useCallback((text: string) => {
    demo.setSource?.(text);
  }, []);
  useEditable(preRef, onChange, {
    indentation: 2,
    disabled: !demo.setSource,
  });

  return (
    <div className={styles.container}>
      {/* Live Demo Preview */}
      <div className={styles.demoSection}>{demo.component}</div>

      {/* Editable Code Section */}
      <div className={styles.codeSection}>
        <div className={styles.header}>
          <div className={styles.headerContainer}>
            <div className={styles.tabContainer}>
              {demo.files.length > 1 ? (
                <Tabs
                  tabs={tabs}
                  selectedTabId={demo.selectedFileName}
                  onTabSelect={demo.selectFileName}
                />
              ) : (
                <span className={styles.fileName}>{demo.selectedFileName}</span>
              )}
            </div>
            <div className={styles.headerActions}>
              {demo.variants.length > 1 && (
                <Select
                  items={variants}
                  value={demo.selectedVariant}
                  onValueChange={demo.selectVariant}
                />
              )}
              {hasJsTransform && (
                <div className={styles.switchContainer}>
                  <LabeledSwitch
                    checked={isJsSelected}
                    onCheckedChange={toggleJs}
                    labels={labels}
                  />
                </div>
              )}
            </div>
          </div>
        </div>

        {/* Editable Code Block */}
        <div className={styles.code}>{demo.selectedFile}</div>
      </div>
    </div>
  );
}

Types

useDemo

ParameterTypeDescription
contentProps
ContentProps<{}>
opts
UseDemoOpts | undefined
Return Type
KeyTypeRequired
component
| string
| number
| bigint
| true
| ReactElement
| Iterable<React.ReactNode, any, any>
| Promise<AwaitedReactNode>
| null
Yes
ref
React.RefObject<HTMLDivElement | null>
Yes
resetFocus
() => void
Yes
openStackBlitz
() => void
Yes
openCodeSandbox
() => void
Yes
name
string | undefined
Yes
slug
string | undefined
Yes
variants
string[]
Yes
selectedVariant
string
Yes
selectVariant
(variant: string | null) => void
Yes
files
{
  name: string;
  slug?: string;
  component: React.ReactNode;
}[]
Yes
selectedFile
React.ReactNode
Yes
selectedFileLines
number
Yes
selectedFileName
string | undefined
Yes
selectFileName
(fileName: string) => void
Yes
allFilesSlugs
{
  fileName: string;
  slug: string;
  variantName: string;
}[]
Yes
expanded
boolean
Yes
expand
() => void
Yes
setExpanded
(expanded: boolean) => void
Yes
copy
(
  event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
) => Promise<void>
Yes
availableTransforms
string[]
Yes
selectedTransform
string | null | undefined
Yes
selectTransform
(transformName: string | null) => void
Yes
setSource
((source: string) => void) | undefined
No
userProps
UserProps<{}>
Yes

createStackBlitz

Create StackBlitz configuration for use with openWithForm

PropertyTypeDescription
title
string
description
string
flattenedFiles
FlattenedFiles
rootFile
string
Return Type
KeyTypeDescription
url
string
formData
Record<string, string>

createCodeSandbox

Utility function for creating CodeSandbox demos Returns the configuration that can be used with openWithForm

PropertyTypeDescription
flattenedFiles
FlattenedFiles
rootFile
string
Return Type
KeyTypeDescription
url
string
formData
Record<string, string>

flattenCodeVariant

Flatten a VariantCode into a flat files structure Resolves relative paths and handles metadata file scoping Uses addPathsToVariant for path resolution logic

ParameterTypeDescription
variant
VariantCode
Return Type

exportVariant

Export a variant as a standalone project with metadata files properly scoped

ParameterTypeDescription
variantCode
VariantCode
config
ExportConfig | undefined
Return Type
KeyTypeDescription
exported
VariantCode
rootFile
string

Additional Types

ExportConfig
type ExportConfig = {
  /** The title for the demo (used in HTML title and package.json name) */
  title?: string;
  /** Optional prefix to add before the title */
  titlePrefix?: string;
  /** Optional suffix to add after the title */
  titleSuffix?: string;
  /** Description for package.json */
  description?: string;
  /** Optional prefix to add before the description */
  descriptionPrefix?: string;
  /** Optional suffix to add after the description */
  descriptionSuffix?: string;
  /** The variant name/identifier for this specific code variant */
  variantName?: string;
  /** Language for the HTML document (default is 'en') */
  language?: string;
  /**
   * Prefix for output file paths (e.g., 'public/' for CRA, '' for Vite)
   * @example htmlPrefix: 'public/' // outputs index.html to correct depth + public/index.html
   */
  htmlPrefix?: string;
  /** Prefix for asset files (e.g., 'assets/' for CRA) */
  assetPrefix?: string;
  /** Prefix for code files (e.g., 'src/' for Vite) */
  sourcePrefix?: string;
  /**
   * Custom HTML template function
   * @example
   * htmlTemplate: ({ language, title, description, head, entrypoint, variant, variantName }) =>
   *   `<!doctype html><html><head><title>${title}</title>${head || ''}</head><body><div id="root"></div><script src="${entrypoint}"></script></body></html>`
   */
  htmlTemplate?: (params: {
    language: string;
    title: string;
    description: string;
    head?: string;
    entrypoint?: string;
    variant?: VariantCode;
    variantName?: string;
  }) => string;
  /**
   * Custom head template function for generating additional head content
   * @example
   * headTemplate: ({ sourcePrefix, assetPrefix, variant, variantName }) =>
   *   `<link rel="stylesheet" href="${assetPrefix}/styles.css" />\n<meta name="theme-color" content="#000000" />`
   */
  headTemplate?: (params: {
    sourcePrefix: string;
    assetPrefix: string;
    variant?: VariantCode;
    variantName?: string;
  }) => string;
  /** Custom React root index template function */
  rootIndexTemplate?: (params: { importString: string; useTypescript: boolean }) => string;
  /** Extra package.json dependencies to add */
  dependencies?: Record<string, string>;
  /** Extra package.json devDependencies to add */
  devDependencies?: Record<string, string>;
  /** Extra package.json scripts to add */
  scripts?: Record<string, string>;
  /** Package type: 'module' for ESM, 'commonjs' for CJS, undefined to omit */
  packageType?: 'module' | 'commonjs';
  /** Custom package.json fields to merge */
  packageJsonFields?: Record<string, any>;
  /** Extra tsconfig.json options to merge */
  tsconfigOptions?: Record<string, any>;
  /** Vite configuration options */
  viteConfig?: Record<string, any>;
  /** Whether to include TypeScript configuration files */
  useTypescript?: boolean;
  /** Custom metadata files to add */
  extraMetadataFiles?: Record<string, { source: string }>;
  /**
   * Whether the framework handles entrypoint and HTML generation (e.g., CRA with webpack)
   * When true, skips generating index.html and entrypoint files
   */
  frameworkHandlesEntrypoint?: boolean;
  /** Whether to skip adding the JavaScript link in the HTML */
  htmlSkipJsLink?: boolean;
  /** Framework-specific files that override default files (index.html, entrypoint, etc.) */
  frameworkFiles?: { variant?: VariantCode; globals?: VariantExtraFiles };
  /**
   * Custom export function to use instead of the default exportVariant or exportVariantAsCra
   * @example exportFunction: (variantCode, config) => ({ exported: customProcessedCode, rootFile: 'custom-entry.js' })
   */
  exportFunction?: (
    variantCode: VariantCode,
    config: ExportConfig,
  ) => { exported: VariantCode; rootFile: string };
  /**
   * Transform function that runs at the very start of the export process
   * Can modify the variant code and metadata before any other processing happens
   * @example
   * transformVariant: (variant, globals, variantName) => ({
   *   variant: { ...variant, source: modifiedSource },
   *   globals: { ...globals, extraFiles: { ...globals.extraFiles, 'theme.css': { source: '.new {}', metadata: true } } }
   * })
   */
  transformVariant?: (
    variant: VariantCode,
    variantName?: string,
    globals?: VariantExtraFiles,
  ) => { variant?: VariantCode; globals?: VariantExtraFiles } | undefined;
  /**
   * Version overrides for core packages (react, react-dom,
   * @types /react,
   * @types /react-dom)
   * @example
   * versions: {
   * '@types/react': '^19',
   * '@types/react-dom': '^19',
   * react: '^19',
   * 'react-dom': '^19',
   * }
   */
  versions?: Record<string, string>;
  /**
   * Custom dependency resolution function
   * @example
   * resolveDependencies: (packageName, envVars) => {
   *   if (packageName === '@mui/material') {
   *     return { '@mui/material': 'latest', '@emotion/react': 'latest' };
   *   }
   *   return { [packageName]: 'latest' };
   * }
   */
  resolveDependencies?: (
    packageName: string,
    envVars?: Record<string, string>,
  ) => Record<string, string>;
}
FlatFile
type FlatFile = { source: string; metadata?: boolean }
FlattenedFiles
type FlattenedFiles = { [filePath: string]: FlatFile }

URL Management and Deep-Linking

Since useDemo extends useCode, it automatically inherits all URL hash management capabilities. This enables powerful deep-linking features for demos with multiple files:

Automatic URL Hash Generation

// Demo with URL-based name/slug generation
const ButtonDemo = createDemo({
  url: 'file:///components/demos/interactive-button/index.ts',
  code: buttonCode,
  components: { Default: ButtonComponent },
  Content: DemoContent,
});

// Automatically generates:
// - name: "Interactive Button"
// - slug: "interactive-button"
// - File URLs: #interactive-button:button.tsx, #interactive-button:styles.css

Deep-Linking to Specific Files

Users can bookmark and share links to specific files within your demos:

# Links to main file in default variant
https://yoursite.com/demos/button#interactive-button:button.tsx

# Links to specific file in TypeScript variant
https://yoursite.com/demos/button#interactive-button:typescript:button.tsx

# Links to styling file
https://yoursite.com/demos/button#interactive-button:styles.css

URL Management Behavior

Initial Load:

  • Hash is read to determine initial file and variant selection
  • Priority: URL hash > localStorage > initialVariant > first variant
  • Demos automatically expand when a relevant hash is present

User Interactions:

  • File Tab Clicks: Hash behavior controlled by fileHashMode option (default: 'remove-hash')
    • 'remove-hash': Removes entire hash on click
    • 'remove-filename': Keeps variant in hash, removes filename
  • Variant Changes: Hash behavior controlled by fileHashMode option
    • 'remove-hash': Removes entire hash on variant switch
    • 'remove-filename': Updates hash to reflect new variant

localStorage Persistence:

  • Controlled by saveHashVariantToLocalStorage option (default: 'on-interaction')
  • 'on-load': Hash variant saved immediately when page loads
  • 'on-interaction': Hash variant saved only when user clicks a file tab
  • 'never': Hash variant never saved to localStorage

Component Integration:

  • Works seamlessly with demo component rendering
  • File selection synchronized with URL hash
  • Variant switching coordinated with hash updates

For detailed information about URL hash patterns and configuration, see the useCode URL Management section.

Integration Patterns

Standard Usage Pattern

The most common pattern is to use useDemo in a content component that receives props from the demo factory:

// In your demo's Content component
export function DemoContent(props: ContentProps<{}>) {
  const demo = useDemo(props, { preClassName: styles.codeBlock }); // Always pass props directly

  return (
    <div className={styles.container}>
      <div className={styles.demoSection}>{demo.component}</div>

      <div className={styles.codeSection}>{demo.selectedFile}</div>
    </div>
  );
}

Demo Factory Integration

This content component is used with the demo factory pattern, not as a direct child of CodeHighlighter:

// ✓ Correct - Demo factory usage
const ButtonDemo = createDemo({
  name: 'Button Demo',
  code: buttonCode,
  components: { Default: ButtonComponent },
  Content: DemoContent,
});

// × Incorrect - Never use DemoContent as a direct child
<CodeHighlighter>
  <DemoContent /> {/* This won't work */}
</CodeHighlighter>;

Best Practices

1. Always Pass Props Directly

export function DemoContent(props: ContentProps<{}>) {
  const demo = useDemo(props, { preClassName: styles.codeBlock }); // ✓ Pass props directly

  // × Never access props.name, props.code, etc.
  // ✓ Use demo.name, demo.selectedFile, etc.

  return (
    <div className={styles.container}>
      <div className={styles.demoSection}>{demo.component}</div>
      <div className={styles.codeSection}>{demo.selectedFile}</div>
    </div>
  );
}

2. Conditional UI Elements

export function DemoContent(props) {
  const demo = useDemo(props, { preClassName: styles.codeBlock });

  return (
    <div className={styles.container}>
      {demo.component}

      {/* Only show file tabs if multiple files */}
      {demo.files.length > 1 ? (
        <Tabs
          tabs={demo.files.map((f) => ({ id: f.name, name: f.name }))}
          selectedTabId={demo.selectedFileName}
          onTabSelect={demo.selectFileName}
        />
      ) : (
        <span>{demo.selectedFileName}</span>
      )}

      {demo.selectedFile}
    </div>
  );
}

3. Simple Transform Toggle

export function DemoContent(props) {
  const demo = useDemo(props, { preClassName: styles.codeBlock });

  const hasTransforms = demo.availableTransforms.length > 0;
  const isJsSelected = demo.selectedTransform === 'js';

  return (
    <div className={styles.container}>
      {demo.component}

      {hasTransforms && (
        <button onClick={() => demo.selectTransform(isJsSelected ? null : 'js')}>
          {isJsSelected ? 'Show TS' : 'Show JS'}
        </button>
      )}

      {demo.selectedFile}
    </div>
  );
}

4. Leverage URL-Based Demo Properties

// Recommended: Let useDemo generate name/slug from URL
const ButtonDemo = createDemo({
  url: 'file:///components/demos/advanced-button/index.ts',
  code: buttonCode,
  components: { Default: ButtonComponent },
  Content: DemoContent,
});

function DemoContent(props) {
  const demo = useDemo(props);

  return (
    <div>
      <h2>{demo.name}</h2> {/* "Advanced Button" */}
      <div data-demo-slug={demo.slug}>
        {' '}
        {/* "advanced-button" */}
        {demo.component}
      </div>
      {/* File navigation with automatic URL hash management */}
      {demo.files.map((file) => (
        <button
          key={file.name}
          onClick={() => demo.selectFileName(file.name)}
          data-file-slug={file.slug} // For analytics/debugging
        >
          {file.name}
        </button>
      ))}
      {demo.selectedFile}
    </div>
  );
}

Common Patterns

Simple Demo Display

The most basic pattern for showing a component with its code:

export function DemoContent(props) {
  const demo = useDemo(props, { preClassName: styles.codeBlock });

  return (
    <div className={styles.container}>
      <div className={styles.demoSection}>{demo.component}</div>
      <div className={styles.codeSection}>{demo.selectedFile}</div>
    </div>
  );
}

Demo with File Navigation

When you have multiple files to show (includes automatic URL hash management):

export function MultiFileDemoContent(props) {
  const demo = useDemo(props, { preClassName: styles.codeBlock });

  return (
    <div className={styles.container}>
      <div className={styles.demoSection}>{demo.component}</div>

      <div className={styles.codeSection}>
        {demo.files.length > 1 && (
          <div className={styles.fileNav}>
            {/* File selection automatically updates URL hash for deep-linking */}
            {demo.files.map((file) => (
              <button
                key={file.name}
                onClick={() => demo.selectFileName(file.name)} // Updates URL hash
                className={demo.selectedFileName === file.name ? styles.active : ''}
                title={`View ${file.name} (URL: #${file.slug})`}
              >
                {file.name}
              </button>
            ))}
          </div>
        )}

        {demo.selectedFile}
      </div>
    </div>
  );
}

Demo with Language Toggle

For demos that support TypeScript/JavaScript switching:

export function DemoWithLanguageToggle(props) {
  const demo = useDemo(props, { preClassName: styles.codeBlock });

  const canToggleJs = demo.availableTransforms.includes('js');
  const showingJs = demo.selectedTransform === 'js';

  return (
    <div className={styles.container}>
      <div className={styles.demoSection}>{demo.component}</div>

      <div className={styles.codeSection}>
        {canToggleJs && (
          <div className={styles.languageToggle}>
            <button onClick={() => demo.selectTransform(showingJs ? null : 'js')}>
              {showingJs ? 'TypeScript' : 'JavaScript'}
            </button>
          </div>
        )}

        {demo.selectedFile}
      </div>
    </div>
  );
}

Performance Considerations

  • Component memoization: Components are automatically memoized by the demo factory
  • Code lazy loading: Inherited from useCode - syntax highlighting can be deferred
  • Transform caching: Transform results are cached for quick switching
  • File switching: File navigation is optimized for instant switching

Error Handling

useDemo inherits error handling from useCode and adds demo-specific safeguards:

  • Missing components: Gracefully handles when components aren't available for a variant
  • Invalid names/slugs: Provides fallback values for missing identification
  • Component render errors: Use React Error Boundaries to catch component-specific issues

Troubleshooting

Component Not Rendering

  • Verify the component is passed in the components prop
  • Check that the variant name matches between code and components
  • Ensure the component doesn't have render-blocking errors

Code Not Showing

Name/Slug Not Generated

  • Ensure a valid url property is provided in demo creation or available in context
  • Provide explicit name or slug properties as fallbacks if URL parsing fails
  • Check that the URL follows a recognizable pattern for automatic generation

URL Hash Issues

  • Deep-linking not working: See useCode URL troubleshooting for detailed debugging steps
  • File navigation not updating URL: Ensure component is running in browser environment (not SSR)
  • Hash conflicts: Check that demo slugs are unique across your application