Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
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.
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.
useCode integration with URL hash routingimport { 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>
);
}
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>
);
}
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>
);
}
useDemo| Parameter | Type | Description |
|---|---|---|
| contentProps | | |
| opts | |
| Key | Type | Required |
|---|---|---|
| component | | Yes |
| ref | | Yes |
| resetFocus | | Yes |
| openStackBlitz | | Yes |
| openCodeSandbox | | Yes |
| name | | Yes |
| slug | | Yes |
| variants | | Yes |
| selectedVariant | | Yes |
| selectVariant | | Yes |
| files | | Yes |
| selectedFile | | Yes |
| selectedFileLines | | Yes |
| selectedFileName | | Yes |
| selectFileName | | Yes |
| allFilesSlugs | | Yes |
| expanded | | Yes |
| expand | | Yes |
| setExpanded | | Yes |
| copy | | Yes |
| availableTransforms | | Yes |
| selectedTransform | | Yes |
| selectTransform | | Yes |
| setSource | | No |
| userProps | | Yes |
createStackBlitzCreate StackBlitz configuration for use with openWithForm
| Property | Type | Description |
|---|---|---|
| title | | |
| description | | |
| flattenedFiles | | |
| rootFile | |
| Key | Type | Description |
|---|---|---|
| url | | |
| formData | |
createCodeSandboxUtility function for creating CodeSandbox demos Returns the configuration that can be used with openWithForm
| Property | Type | Description |
|---|---|---|
| flattenedFiles | | |
| rootFile | |
| Key | Type | Description |
|---|---|---|
| url | | |
| formData | |
flattenCodeVariantFlatten a VariantCode into a flat files structure Resolves relative paths and handles metadata file scoping Uses addPathsToVariant for path resolution logic
| Parameter | Type | Description |
|---|---|---|
| variant | |
exportVariantExport a variant as a standalone project with metadata files properly scoped
| Parameter | Type | Description |
|---|---|---|
| variantCode | | |
| config | |
| Key | Type | Description |
|---|---|---|
| exported | | |
| rootFile | |
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>;
}type FlatFile = { source: string; metadata?: boolean }type FlattenedFiles = { [filePath: string]: FlatFile }Since useDemo extends useCode, it automatically inherits all URL hash management capabilities. This enables powerful deep-linking features for demos with multiple files:
// 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
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
Initial Load:
initialVariant > first variantUser Interactions:
fileHashMode option (default: 'remove-hash')
'remove-hash': Removes entire hash on click'remove-filename': Keeps variant in hash, removes filenamefileHashMode option
'remove-hash': Removes entire hash on variant switch'remove-filename': Updates hash to reflect new variantlocalStorage Persistence:
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 localStorageComponent Integration:
For detailed information about URL hash patterns and configuration, see the useCode URL Management section.
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>
);
}
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>;
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>
);
}
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>
);
}
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>
);
}
// 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>
);
}
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>
);
}
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>
);
}
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>
);
}
useCode - syntax highlighting can be deferreduseDemo inherits error handling from useCode and adds demo-specific safeguards:
components propcode and componentscontentProps contains the expected code structureurl property is provided in demo creation or available in contextname or slug properties as fallbacks if URL parsing fails