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>
);
}
contentProps: ContentProps<T>The content properties passed from your parent component - these should always be passed directly to useDemo() without accessing them directly:
export function DemoContent(props: ContentProps<{}>) {
const demo = useDemo(props); // Pass props directly to useDemo
// Never access props.name, props.code, etc. directly
// Use demo.name, demo.selectedFile, etc. instead
}
opts?: UseDemoOptsOptional configuration object:
interface UseDemoOpts {
defaultOpen?: boolean; // Whether to start expanded
copy?: UseCopierOpts; // Copy functionality options
githubUrlPrefix?: string; // GitHub URL prefix for links
codeSandboxUrlPrefix?: string; // CodeSandbox URL prefix
stackBlitzPrefix?: string; // StackBlitz URL prefix
initialVariant?: string; // Initially selected variant
initialTransform?: string; // Initially selected transform
fileHashMode?: 'remove-hash' | 'remove-filename'; // Controls hash removal on user interaction
saveHashVariantToLocalStorage?: 'on-load' | 'on-interaction' | 'never'; // When to persist hash variant
}
The hook returns all useCode properties plus demo-specific additions:
component: React.ReactNode - The React component for the current variant (for live preview)ref: React.RefObject<HTMLDivElement | null> - Ref for the demo container elementresetFocus: () => void - Function to reset focus to the demo containername: string | undefined - Display name for the demo (auto-generated from URL if not provided)slug: string | undefined - URL-friendly identifier (auto-generated from URL if not provided)All properties from useCode are available with exact types:
variants: string[], selectedVariant: string, selectVariant: React.Dispatch<React.SetStateAction<string>>files: Array<{ name: string; slug?: string; component: React.ReactNode }> - Files with URL hash slugs for deep-linkingselectedFile: React.ReactNode, selectedFileName: string | undefined, selectFileName: (fileName: string) => void - File selection with automatic URL hash updatesexpanded: boolean, expand: () => void, setExpanded: React.Dispatch<React.SetStateAction<boolean>>copy: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void>availableTransforms: string[], selectedTransform: string | null | undefined, selectTransform: (transformName: string | null) => voidsetSource?: (source: string) => void (when editing is available)userProps: UserProps<T> - Generated user properties including name, slug, and custom propsSee useCode documentation for detailed information about inherited functionality.
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