Code
Code Block
Advanced syntax highlighting with line numbers and clipboard functionality. Perfect for React documentation requiring code display with Next.js integration and TypeScript support.
Powered by
Loading component...
'use client';import type { BundledLanguage } from '@/components/ui/shadcn-io/code-block';import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockFilename, CodeBlockFiles, CodeBlockHeader, CodeBlockItem, CodeBlockSelect, CodeBlockSelectContent, CodeBlockSelectItem, CodeBlockSelectTrigger, CodeBlockSelectValue,} from '@/components/ui/shadcn-io/code-block';const code = [ { language: 'jsx', filename: 'MyComponent.jsx', code: `function MyComponent(props) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, }, { language: 'tsx', filename: 'MyComponent.tsx', code: `function MyComponent(props: { name: string }) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, },];const Example = () => ( <CodeBlock data={code} defaultValue={code[0].language}> <CodeBlockHeader> <CodeBlockFiles> {(item) => ( <CodeBlockFilename key={item.language} value={item.language}> {item.filename} </CodeBlockFilename> )} </CodeBlockFiles> <CodeBlockSelect> <CodeBlockSelectTrigger> <CodeBlockSelectValue /> </CodeBlockSelectTrigger> <CodeBlockSelectContent> {(item) => ( <CodeBlockSelectItem key={item.language} value={item.language}> {item.language} </CodeBlockSelectItem> )} </CodeBlockSelectContent> </CodeBlockSelect> <CodeBlockCopyButton onCopy={() => console.log('Copied code to clipboard')} onError={() => console.error('Failed to copy code to clipboard')} /> </CodeBlockHeader> <CodeBlockBody> {(item) => ( <CodeBlockItem key={item.language} value={item.language}> <CodeBlockContent language={item.language as BundledLanguage}> {item.code} </CodeBlockContent> </CodeBlockItem> )} </CodeBlockBody> </CodeBlock>);export default Example;
'use client';import { type IconType, SiAstro, SiBiome, SiBower, SiBun, SiC, SiCircleci, SiCoffeescript, SiCplusplus, SiCss, SiCssmodules, SiDart, SiDocker, SiDocusaurus, SiDotenv, SiEditorconfig, SiEslint, SiGatsby, SiGitignoredotio, SiGnubash, SiGo, SiGraphql, SiGrunt, SiGulp, SiHandlebarsdotjs, SiHtml5, SiJavascript, SiJest, SiJson, SiLess, SiMarkdown, SiMdx, SiMintlify, SiMocha, SiMysql, SiNextdotjs, SiPerl, SiPhp, SiPostcss, SiPrettier, SiPrisma, SiPug, SiPython, SiR, SiReact, SiReadme, SiRedis, SiRemix, SiRive, SiRollupdotjs, SiRuby, SiSanity, SiSass, SiScala, SiSentry, SiShadcnui, SiStorybook, SiStylelint, SiSublimetext, SiSvelte, SiSvg, SiSwift, SiTailwindcss, SiToml, SiTypescript, SiVercel, SiVite, SiVuedotjs, SiWebassembly,} from '@icons-pack/react-simple-icons';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { CheckIcon, CopyIcon } from 'lucide-react';import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode,} from 'react';import { cloneElement, createContext, useContext, useEffect, useState,} from 'react';import { Button } from '@/components/ui/button';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';import { cn } from '@/lib/utils';export type BundledLanguage = string;const filenameIconMap = { '.env': SiDotenv, '*.astro': SiAstro, 'biome.json': SiBiome, '.bowerrc': SiBower, 'bun.lockb': SiBun, '*.c': SiC, '*.cpp': SiCplusplus, '.circleci/config.yml': SiCircleci, '*.coffee': SiCoffeescript, '*.module.css': SiCssmodules, '*.css': SiCss, '*.dart': SiDart, Dockerfile: SiDocker, 'docusaurus.config.js': SiDocusaurus, '.editorconfig': SiEditorconfig, '.eslintrc': SiEslint, 'eslint.config.*': SiEslint, 'gatsby-config.*': SiGatsby, '.gitignore': SiGitignoredotio, '*.go': SiGo, '*.graphql': SiGraphql, '*.sh': SiGnubash, 'Gruntfile.*': SiGrunt, 'gulpfile.*': SiGulp, '*.hbs': SiHandlebarsdotjs, '*.html': SiHtml5, '*.js': SiJavascript, '*.json': SiJson, '*.test.js': SiJest, '*.less': SiLess, '*.md': SiMarkdown, '*.mdx': SiMdx, 'mintlify.json': SiMintlify, 'mocha.opts': SiMocha, '*.mustache': SiHandlebarsdotjs, '*.sql': SiMysql, 'next.config.*': SiNextdotjs, '*.pl': SiPerl, '*.php': SiPhp, 'postcss.config.*': SiPostcss, 'prettier.config.*': SiPrettier, '*.prisma': SiPrisma, '*.pug': SiPug, '*.py': SiPython, '*.r': SiR, '*.rb': SiRuby, '*.jsx': SiReact, '*.tsx': SiReact, 'readme.md': SiReadme, '*.rdb': SiRedis, 'remix.config.*': SiRemix, '*.riv': SiRive, 'rollup.config.*': SiRollupdotjs, 'sanity.config.*': SiSanity, '*.sass': SiSass, '*.scss': SiSass, '*.sc': SiScala, '*.scala': SiScala, 'sentry.client.config.*': SiSentry, 'components.json': SiShadcnui, 'storybook.config.*': SiStorybook, 'stylelint.config.*': SiStylelint, '.sublime-settings': SiSublimetext, '*.svelte': SiSvelte, '*.svg': SiSvg, '*.swift': SiSwift, 'tailwind.config.*': SiTailwindcss, '*.toml': SiToml, '*.ts': SiTypescript, 'vercel.json': SiVercel, 'vite.config.*': SiVite, '*.vue': SiVuedotjs, '*.wasm': SiWebassembly,};const lineNumberClassNames = cn( '[&_code]:[counter-reset:line]', '[&_code]:[counter-increment:line_0]', '[&_.line]:before:content-[counter(line)]', '[&_.line]:before:inline-block', '[&_.line]:before:[counter-increment:line]', '[&_.line]:before:w-4', '[&_.line]:before:mr-4', '[&_.line]:before:text-[13px]', '[&_.line]:before:text-right', '[&_.line]:before:text-muted-foreground/50', '[&_.line]:before:font-mono', '[&_.line]:before:select-none');const darkModeClassNames = cn( 'dark:[&_.shiki]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]', 'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]', 'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]');const lineHighlightClassNames = cn( '[&_.line.highlighted]:bg-blue-50', '[&_.line.highlighted]:after:bg-blue-500', '[&_.line.highlighted]:after:absolute', '[&_.line.highlighted]:after:left-0', '[&_.line.highlighted]:after:top-0', '[&_.line.highlighted]:after:bottom-0', '[&_.line.highlighted]:after:w-0.5', 'dark:[&_.line.highlighted]:!bg-blue-500/10');const lineDiffClassNames = cn( '[&_.line.diff]:after:absolute', '[&_.line.diff]:after:left-0', '[&_.line.diff]:after:top-0', '[&_.line.diff]:after:bottom-0', '[&_.line.diff]:after:w-0.5', '[&_.line.diff.add]:bg-emerald-50', '[&_.line.diff.add]:after:bg-emerald-500', '[&_.line.diff.remove]:bg-rose-50', '[&_.line.diff.remove]:after:bg-rose-500', 'dark:[&_.line.diff.add]:!bg-emerald-500/10', 'dark:[&_.line.diff.remove]:!bg-rose-500/10');const lineFocusedClassNames = cn( '[&_code:has(.focused)_.line]:blur-[2px]', '[&_code:has(.focused)_.line.focused]:blur-none');const wordHighlightClassNames = cn( '[&_.highlighted-word]:bg-blue-50', 'dark:[&_.highlighted-word]:!bg-blue-500/10');const codeBlockClassName = cn( 'mt-0 bg-background text-sm', '[&_pre]:py-4', '[&_.shiki]:!bg-[var(--shiki-bg)]', '[&_code]:w-full', '[&_code]:grid', '[&_code]:overflow-x-auto', '[&_code]:bg-transparent', '[&_.line]:px-4', '[&_.line]:w-full', '[&_.line]:relative');type CodeBlockData = { language: string; filename: string; code: string;};type CodeBlockContextType = { value: string | undefined; onValueChange: ((value: string) => void) | undefined; data: CodeBlockData[];};const CodeBlockContext = createContext<CodeBlockContextType>({ value: undefined, onValueChange: undefined, data: [],});export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; data: CodeBlockData[];};export const CodeBlock = ({ value: controlledValue, onValueChange: controlledOnValueChange, defaultValue, className, data, ...props}: CodeBlockProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); return ( <CodeBlockContext.Provider value={{ value, onValueChange, data }}> <div className={cn('size-full overflow-hidden rounded-md border', className)} {...props} /> </CodeBlockContext.Provider> );};export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;export const CodeBlockHeader = ({ className, ...props}: CodeBlockHeaderProps) => ( <div className={cn( 'flex flex-row items-center border-b bg-secondary p-1', className )} {...props} />);export type CodeBlockFilesProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockFiles = ({ className, children, ...props}: CodeBlockFilesProps) => { const { data } = useContext(CodeBlockContext); return ( <div className={cn('flex grow flex-row items-center gap-2', className)} {...props} > {data.map(children)} </div> );};export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & { icon?: IconType; value?: string;};export const CodeBlockFilename = ({ className, icon, value, children, ...props}: CodeBlockFilenameProps) => { const { value: activeValue } = useContext(CodeBlockContext); const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => { const regex = new RegExp( `^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$` ); return regex.test(children as string); })?.[1]; const Icon = icon ?? defaultIcon; if (value !== activeValue) { return null; } return ( <div className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs" {...props} > {Icon && <Icon className="h-4 w-4 shrink-0" />} <span className="flex-1 truncate">{children}</span> </div> );};export type CodeBlockSelectProps = ComponentProps<typeof Select>;export const CodeBlockSelect = (props: CodeBlockSelectProps) => { const { value, onValueChange } = useContext(CodeBlockContext); return <Select onValueChange={onValueChange} value={value} {...props} />;};export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;export const CodeBlockSelectTrigger = ({ className, ...props}: CodeBlockSelectTriggerProps) => ( <SelectTrigger className={cn( 'w-fit border-none text-muted-foreground text-xs shadow-none', className )} {...props} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...props} />);export type CodeBlockSelectContentProps = Omit< ComponentProps<typeof SelectContent>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockSelectContent = ({ children, ...props}: CodeBlockSelectContentProps) => { const { data } = useContext(CodeBlockContext); return <SelectContent {...props}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...props} />);export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};export const CodeBlockCopyButton = ({ asChild, onCopy, onError, timeout = 2000, children, className, ...props}: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const { data, value } = useContext(CodeBlockContext); const code = data.find((item) => item.language === value)?.code; const copyToClipboard = () => { if ( typeof window === 'undefined' || !navigator.clipboard.writeText || !code ) { return; } navigator.clipboard.writeText(code).then(() => { setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); }, onError); }; if (asChild) { return cloneElement(children as ReactElement, { // @ts-expect-error - we know this is a button onClick: copyToClipboard, }); } const Icon = isCopied ? CheckIcon : CopyIcon; return ( <Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...props}> <pre className="w-full"> <code> {children ?.toString() .split('\n') .map((line, i) => ( <span className="line" key={i}> {line} </span> ))} </code> </pre> </div>);export type CodeBlockBodyProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => { const { data } = useContext(CodeBlockContext); return <div {...props}>{data.map(children)}</div>;};export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & { value: string; lineNumbers?: boolean;};export const CodeBlockItem = ({ children, lineNumbers = true, className, value, ...props}: CodeBlockItemProps) => { const { value: activeValue } = useContext(CodeBlockContext); if (value !== activeValue) { return null; } return ( <div className={cn( codeBlockClassName, lineHighlightClassNames, lineDiffClassNames, lineFocusedClassNames, wordHighlightClassNames, darkModeClassNames, lineNumbers && lineNumberClassNames, className )} {...props} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: any; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes, language, syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;};
Installation
npx shadcn@latest add https://www.shadcn.io/registry/code-block.json
npx shadcn@latest add https://www.shadcn.io/registry/code-block.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/code-block.json
bunx shadcn@latest add https://www.shadcn.io/registry/code-block.json
Features
- Advanced syntax highlighting powered by Shiki with support for 100+ programming languages
- Interactive line numbers with customizable display and selection capabilities
- One-click clipboard functionality using native browser APIs with success feedback
- Filename display support showing file headers with language icons and metadata
- Flexible highlighting system supporting line ranges, word patterns, and focused sections
- Diff visualization displaying code changes with addition and deletion indicators
- Automatic language detection with manual override options for precise syntax parsing
- Theme customization supporting light/dark modes and custom color schemes
- TypeScript support with complete interface definitions for reliable integration
- Server-side rendering compatible with both client and server component architectures
Examples
No header
Loading component...
'use client';import type { BundledLanguage } from '@/components/ui/shadcn-io/code-block';import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockItem,} from '@/components/ui/shadcn-io/code-block';const code = [ { language: 'jsx', filename: 'MyComponent.jsx', code: `function MyComponent(props) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, }, { language: 'tsx', filename: 'MyComponent.tsx', code: `function MyComponent(props: { name: string }) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, },];const Example = () => ( <CodeBlock data={code} defaultValue={code[0].language}> <CodeBlockBody> {(item) => ( <CodeBlockItem key={item.language} value={item.language}> <CodeBlockContent language={item.language as BundledLanguage}> {item.code} </CodeBlockContent> </CodeBlockItem> )} </CodeBlockBody> </CodeBlock>);export default Example;
'use client';import { type IconType, SiAstro, SiBiome, SiBower, SiBun, SiC, SiCircleci, SiCoffeescript, SiCplusplus, SiCss, SiCssmodules, SiDart, SiDocker, SiDocusaurus, SiDotenv, SiEditorconfig, SiEslint, SiGatsby, SiGitignoredotio, SiGnubash, SiGo, SiGraphql, SiGrunt, SiGulp, SiHandlebarsdotjs, SiHtml5, SiJavascript, SiJest, SiJson, SiLess, SiMarkdown, SiMdx, SiMintlify, SiMocha, SiMysql, SiNextdotjs, SiPerl, SiPhp, SiPostcss, SiPrettier, SiPrisma, SiPug, SiPython, SiR, SiReact, SiReadme, SiRedis, SiRemix, SiRive, SiRollupdotjs, SiRuby, SiSanity, SiSass, SiScala, SiSentry, SiShadcnui, SiStorybook, SiStylelint, SiSublimetext, SiSvelte, SiSvg, SiSwift, SiTailwindcss, SiToml, SiTypescript, SiVercel, SiVite, SiVuedotjs, SiWebassembly,} from '@icons-pack/react-simple-icons';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { CheckIcon, CopyIcon } from 'lucide-react';import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode,} from 'react';import { cloneElement, createContext, useContext, useEffect, useState,} from 'react';import { Button } from '@/components/ui/button';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';import { cn } from '@/lib/utils';export type BundledLanguage = string;const filenameIconMap = { '.env': SiDotenv, '*.astro': SiAstro, 'biome.json': SiBiome, '.bowerrc': SiBower, 'bun.lockb': SiBun, '*.c': SiC, '*.cpp': SiCplusplus, '.circleci/config.yml': SiCircleci, '*.coffee': SiCoffeescript, '*.module.css': SiCssmodules, '*.css': SiCss, '*.dart': SiDart, Dockerfile: SiDocker, 'docusaurus.config.js': SiDocusaurus, '.editorconfig': SiEditorconfig, '.eslintrc': SiEslint, 'eslint.config.*': SiEslint, 'gatsby-config.*': SiGatsby, '.gitignore': SiGitignoredotio, '*.go': SiGo, '*.graphql': SiGraphql, '*.sh': SiGnubash, 'Gruntfile.*': SiGrunt, 'gulpfile.*': SiGulp, '*.hbs': SiHandlebarsdotjs, '*.html': SiHtml5, '*.js': SiJavascript, '*.json': SiJson, '*.test.js': SiJest, '*.less': SiLess, '*.md': SiMarkdown, '*.mdx': SiMdx, 'mintlify.json': SiMintlify, 'mocha.opts': SiMocha, '*.mustache': SiHandlebarsdotjs, '*.sql': SiMysql, 'next.config.*': SiNextdotjs, '*.pl': SiPerl, '*.php': SiPhp, 'postcss.config.*': SiPostcss, 'prettier.config.*': SiPrettier, '*.prisma': SiPrisma, '*.pug': SiPug, '*.py': SiPython, '*.r': SiR, '*.rb': SiRuby, '*.jsx': SiReact, '*.tsx': SiReact, 'readme.md': SiReadme, '*.rdb': SiRedis, 'remix.config.*': SiRemix, '*.riv': SiRive, 'rollup.config.*': SiRollupdotjs, 'sanity.config.*': SiSanity, '*.sass': SiSass, '*.scss': SiSass, '*.sc': SiScala, '*.scala': SiScala, 'sentry.client.config.*': SiSentry, 'components.json': SiShadcnui, 'storybook.config.*': SiStorybook, 'stylelint.config.*': SiStylelint, '.sublime-settings': SiSublimetext, '*.svelte': SiSvelte, '*.svg': SiSvg, '*.swift': SiSwift, 'tailwind.config.*': SiTailwindcss, '*.toml': SiToml, '*.ts': SiTypescript, 'vercel.json': SiVercel, 'vite.config.*': SiVite, '*.vue': SiVuedotjs, '*.wasm': SiWebassembly,};const lineNumberClassNames = cn( '[&_code]:[counter-reset:line]', '[&_code]:[counter-increment:line_0]', '[&_.line]:before:content-[counter(line)]', '[&_.line]:before:inline-block', '[&_.line]:before:[counter-increment:line]', '[&_.line]:before:w-4', '[&_.line]:before:mr-4', '[&_.line]:before:text-[13px]', '[&_.line]:before:text-right', '[&_.line]:before:text-muted-foreground/50', '[&_.line]:before:font-mono', '[&_.line]:before:select-none');const darkModeClassNames = cn( 'dark:[&_.shiki]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]', 'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]', 'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]');const lineHighlightClassNames = cn( '[&_.line.highlighted]:bg-blue-50', '[&_.line.highlighted]:after:bg-blue-500', '[&_.line.highlighted]:after:absolute', '[&_.line.highlighted]:after:left-0', '[&_.line.highlighted]:after:top-0', '[&_.line.highlighted]:after:bottom-0', '[&_.line.highlighted]:after:w-0.5', 'dark:[&_.line.highlighted]:!bg-blue-500/10');const lineDiffClassNames = cn( '[&_.line.diff]:after:absolute', '[&_.line.diff]:after:left-0', '[&_.line.diff]:after:top-0', '[&_.line.diff]:after:bottom-0', '[&_.line.diff]:after:w-0.5', '[&_.line.diff.add]:bg-emerald-50', '[&_.line.diff.add]:after:bg-emerald-500', '[&_.line.diff.remove]:bg-rose-50', '[&_.line.diff.remove]:after:bg-rose-500', 'dark:[&_.line.diff.add]:!bg-emerald-500/10', 'dark:[&_.line.diff.remove]:!bg-rose-500/10');const lineFocusedClassNames = cn( '[&_code:has(.focused)_.line]:blur-[2px]', '[&_code:has(.focused)_.line.focused]:blur-none');const wordHighlightClassNames = cn( '[&_.highlighted-word]:bg-blue-50', 'dark:[&_.highlighted-word]:!bg-blue-500/10');const codeBlockClassName = cn( 'mt-0 bg-background text-sm', '[&_pre]:py-4', '[&_.shiki]:!bg-[var(--shiki-bg)]', '[&_code]:w-full', '[&_code]:grid', '[&_code]:overflow-x-auto', '[&_code]:bg-transparent', '[&_.line]:px-4', '[&_.line]:w-full', '[&_.line]:relative');type CodeBlockData = { language: string; filename: string; code: string;};type CodeBlockContextType = { value: string | undefined; onValueChange: ((value: string) => void) | undefined; data: CodeBlockData[];};const CodeBlockContext = createContext<CodeBlockContextType>({ value: undefined, onValueChange: undefined, data: [],});export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; data: CodeBlockData[];};export const CodeBlock = ({ value: controlledValue, onValueChange: controlledOnValueChange, defaultValue, className, data, ...props}: CodeBlockProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); return ( <CodeBlockContext.Provider value={{ value, onValueChange, data }}> <div className={cn('size-full overflow-hidden rounded-md border', className)} {...props} /> </CodeBlockContext.Provider> );};export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;export const CodeBlockHeader = ({ className, ...props}: CodeBlockHeaderProps) => ( <div className={cn( 'flex flex-row items-center border-b bg-secondary p-1', className )} {...props} />);export type CodeBlockFilesProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockFiles = ({ className, children, ...props}: CodeBlockFilesProps) => { const { data } = useContext(CodeBlockContext); return ( <div className={cn('flex grow flex-row items-center gap-2', className)} {...props} > {data.map(children)} </div> );};export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & { icon?: IconType; value?: string;};export const CodeBlockFilename = ({ className, icon, value, children, ...props}: CodeBlockFilenameProps) => { const { value: activeValue } = useContext(CodeBlockContext); const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => { const regex = new RegExp( `^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$` ); return regex.test(children as string); })?.[1]; const Icon = icon ?? defaultIcon; if (value !== activeValue) { return null; } return ( <div className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs" {...props} > {Icon && <Icon className="h-4 w-4 shrink-0" />} <span className="flex-1 truncate">{children}</span> </div> );};export type CodeBlockSelectProps = ComponentProps<typeof Select>;export const CodeBlockSelect = (props: CodeBlockSelectProps) => { const { value, onValueChange } = useContext(CodeBlockContext); return <Select onValueChange={onValueChange} value={value} {...props} />;};export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;export const CodeBlockSelectTrigger = ({ className, ...props}: CodeBlockSelectTriggerProps) => ( <SelectTrigger className={cn( 'w-fit border-none text-muted-foreground text-xs shadow-none', className )} {...props} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...props} />);export type CodeBlockSelectContentProps = Omit< ComponentProps<typeof SelectContent>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockSelectContent = ({ children, ...props}: CodeBlockSelectContentProps) => { const { data } = useContext(CodeBlockContext); return <SelectContent {...props}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...props} />);export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};export const CodeBlockCopyButton = ({ asChild, onCopy, onError, timeout = 2000, children, className, ...props}: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const { data, value } = useContext(CodeBlockContext); const code = data.find((item) => item.language === value)?.code; const copyToClipboard = () => { if ( typeof window === 'undefined' || !navigator.clipboard.writeText || !code ) { return; } navigator.clipboard.writeText(code).then(() => { setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); }, onError); }; if (asChild) { return cloneElement(children as ReactElement, { // @ts-expect-error - we know this is a button onClick: copyToClipboard, }); } const Icon = isCopied ? CheckIcon : CopyIcon; return ( <Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...props}> <pre className="w-full"> <code> {children ?.toString() .split('\n') .map((line, i) => ( <span className="line" key={i}> {line} </span> ))} </code> </pre> </div>);export type CodeBlockBodyProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => { const { data } = useContext(CodeBlockContext); return <div {...props}>{data.map(children)}</div>;};export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & { value: string; lineNumbers?: boolean;};export const CodeBlockItem = ({ children, lineNumbers = true, className, value, ...props}: CodeBlockItemProps) => { const { value: activeValue } = useContext(CodeBlockContext); if (value !== activeValue) { return null; } return ( <div className={cn( codeBlockClassName, lineHighlightClassNames, lineDiffClassNames, lineFocusedClassNames, wordHighlightClassNames, darkModeClassNames, lineNumbers && lineNumberClassNames, className )} {...props} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: any; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes, language, syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;};
Highlighted lines
Loading component...
'use client';import type { BundledLanguage } from '@/components/ui/shadcn-io/code-block';import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockFilename, CodeBlockFiles, CodeBlockHeader, CodeBlockItem, CodeBlockSelect, CodeBlockSelectContent, CodeBlockSelectItem, CodeBlockSelectTrigger, CodeBlockSelectValue,} from '@/components/ui/shadcn-io/code-block';const code = [ { language: 'jsx', filename: 'MyComponent.jsx', code: `function MyComponent(props) { // [!code highlight] return ( <div> <h1>Hello, {props.name}!</h1> // [!code highlight] <p>This is an example React component.</p> </div> );}`, }, { language: 'tsx', filename: 'MyComponent.tsx', code: `function MyComponent(props: { name: string }) { // [!code highlight] return ( <div> <h1>Hello, {props.name}!</h1> // [!code highlight] <p>This is an example React component.</p> </div> );}`, },];const Example = () => ( <CodeBlock data={code} defaultValue={code[0].language}> <CodeBlockHeader> <CodeBlockFiles> {(item) => ( <CodeBlockFilename key={item.language} value={item.language}> {item.filename} </CodeBlockFilename> )} </CodeBlockFiles> <CodeBlockSelect> <CodeBlockSelectTrigger> <CodeBlockSelectValue /> </CodeBlockSelectTrigger> <CodeBlockSelectContent> {(item) => ( <CodeBlockSelectItem key={item.language} value={item.language}> {item.language} </CodeBlockSelectItem> )} </CodeBlockSelectContent> </CodeBlockSelect> <CodeBlockCopyButton onCopy={() => console.log('Copied code to clipboard')} onError={() => console.error('Failed to copy code to clipboard')} /> </CodeBlockHeader> <CodeBlockBody> {(item) => ( <CodeBlockItem key={item.language} value={item.language}> <CodeBlockContent language={item.language as BundledLanguage}> {item.code} </CodeBlockContent> </CodeBlockItem> )} </CodeBlockBody> </CodeBlock>);export default Example;
'use client';import { type IconType, SiAstro, SiBiome, SiBower, SiBun, SiC, SiCircleci, SiCoffeescript, SiCplusplus, SiCss, SiCssmodules, SiDart, SiDocker, SiDocusaurus, SiDotenv, SiEditorconfig, SiEslint, SiGatsby, SiGitignoredotio, SiGnubash, SiGo, SiGraphql, SiGrunt, SiGulp, SiHandlebarsdotjs, SiHtml5, SiJavascript, SiJest, SiJson, SiLess, SiMarkdown, SiMdx, SiMintlify, SiMocha, SiMysql, SiNextdotjs, SiPerl, SiPhp, SiPostcss, SiPrettier, SiPrisma, SiPug, SiPython, SiR, SiReact, SiReadme, SiRedis, SiRemix, SiRive, SiRollupdotjs, SiRuby, SiSanity, SiSass, SiScala, SiSentry, SiShadcnui, SiStorybook, SiStylelint, SiSublimetext, SiSvelte, SiSvg, SiSwift, SiTailwindcss, SiToml, SiTypescript, SiVercel, SiVite, SiVuedotjs, SiWebassembly,} from '@icons-pack/react-simple-icons';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { CheckIcon, CopyIcon } from 'lucide-react';import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode,} from 'react';import { cloneElement, createContext, useContext, useEffect, useState,} from 'react';import { Button } from '@/components/ui/button';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';import { cn } from '@/lib/utils';export type BundledLanguage = string;const filenameIconMap = { '.env': SiDotenv, '*.astro': SiAstro, 'biome.json': SiBiome, '.bowerrc': SiBower, 'bun.lockb': SiBun, '*.c': SiC, '*.cpp': SiCplusplus, '.circleci/config.yml': SiCircleci, '*.coffee': SiCoffeescript, '*.module.css': SiCssmodules, '*.css': SiCss, '*.dart': SiDart, Dockerfile: SiDocker, 'docusaurus.config.js': SiDocusaurus, '.editorconfig': SiEditorconfig, '.eslintrc': SiEslint, 'eslint.config.*': SiEslint, 'gatsby-config.*': SiGatsby, '.gitignore': SiGitignoredotio, '*.go': SiGo, '*.graphql': SiGraphql, '*.sh': SiGnubash, 'Gruntfile.*': SiGrunt, 'gulpfile.*': SiGulp, '*.hbs': SiHandlebarsdotjs, '*.html': SiHtml5, '*.js': SiJavascript, '*.json': SiJson, '*.test.js': SiJest, '*.less': SiLess, '*.md': SiMarkdown, '*.mdx': SiMdx, 'mintlify.json': SiMintlify, 'mocha.opts': SiMocha, '*.mustache': SiHandlebarsdotjs, '*.sql': SiMysql, 'next.config.*': SiNextdotjs, '*.pl': SiPerl, '*.php': SiPhp, 'postcss.config.*': SiPostcss, 'prettier.config.*': SiPrettier, '*.prisma': SiPrisma, '*.pug': SiPug, '*.py': SiPython, '*.r': SiR, '*.rb': SiRuby, '*.jsx': SiReact, '*.tsx': SiReact, 'readme.md': SiReadme, '*.rdb': SiRedis, 'remix.config.*': SiRemix, '*.riv': SiRive, 'rollup.config.*': SiRollupdotjs, 'sanity.config.*': SiSanity, '*.sass': SiSass, '*.scss': SiSass, '*.sc': SiScala, '*.scala': SiScala, 'sentry.client.config.*': SiSentry, 'components.json': SiShadcnui, 'storybook.config.*': SiStorybook, 'stylelint.config.*': SiStylelint, '.sublime-settings': SiSublimetext, '*.svelte': SiSvelte, '*.svg': SiSvg, '*.swift': SiSwift, 'tailwind.config.*': SiTailwindcss, '*.toml': SiToml, '*.ts': SiTypescript, 'vercel.json': SiVercel, 'vite.config.*': SiVite, '*.vue': SiVuedotjs, '*.wasm': SiWebassembly,};const lineNumberClassNames = cn( '[&_code]:[counter-reset:line]', '[&_code]:[counter-increment:line_0]', '[&_.line]:before:content-[counter(line)]', '[&_.line]:before:inline-block', '[&_.line]:before:[counter-increment:line]', '[&_.line]:before:w-4', '[&_.line]:before:mr-4', '[&_.line]:before:text-[13px]', '[&_.line]:before:text-right', '[&_.line]:before:text-muted-foreground/50', '[&_.line]:before:font-mono', '[&_.line]:before:select-none');const darkModeClassNames = cn( 'dark:[&_.shiki]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]', 'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]', 'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]');const lineHighlightClassNames = cn( '[&_.line.highlighted]:bg-blue-50', '[&_.line.highlighted]:after:bg-blue-500', '[&_.line.highlighted]:after:absolute', '[&_.line.highlighted]:after:left-0', '[&_.line.highlighted]:after:top-0', '[&_.line.highlighted]:after:bottom-0', '[&_.line.highlighted]:after:w-0.5', 'dark:[&_.line.highlighted]:!bg-blue-500/10');const lineDiffClassNames = cn( '[&_.line.diff]:after:absolute', '[&_.line.diff]:after:left-0', '[&_.line.diff]:after:top-0', '[&_.line.diff]:after:bottom-0', '[&_.line.diff]:after:w-0.5', '[&_.line.diff.add]:bg-emerald-50', '[&_.line.diff.add]:after:bg-emerald-500', '[&_.line.diff.remove]:bg-rose-50', '[&_.line.diff.remove]:after:bg-rose-500', 'dark:[&_.line.diff.add]:!bg-emerald-500/10', 'dark:[&_.line.diff.remove]:!bg-rose-500/10');const lineFocusedClassNames = cn( '[&_code:has(.focused)_.line]:blur-[2px]', '[&_code:has(.focused)_.line.focused]:blur-none');const wordHighlightClassNames = cn( '[&_.highlighted-word]:bg-blue-50', 'dark:[&_.highlighted-word]:!bg-blue-500/10');const codeBlockClassName = cn( 'mt-0 bg-background text-sm', '[&_pre]:py-4', '[&_.shiki]:!bg-[var(--shiki-bg)]', '[&_code]:w-full', '[&_code]:grid', '[&_code]:overflow-x-auto', '[&_code]:bg-transparent', '[&_.line]:px-4', '[&_.line]:w-full', '[&_.line]:relative');type CodeBlockData = { language: string; filename: string; code: string;};type CodeBlockContextType = { value: string | undefined; onValueChange: ((value: string) => void) | undefined; data: CodeBlockData[];};const CodeBlockContext = createContext<CodeBlockContextType>({ value: undefined, onValueChange: undefined, data: [],});export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; data: CodeBlockData[];};export const CodeBlock = ({ value: controlledValue, onValueChange: controlledOnValueChange, defaultValue, className, data, ...props}: CodeBlockProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); return ( <CodeBlockContext.Provider value={{ value, onValueChange, data }}> <div className={cn('size-full overflow-hidden rounded-md border', className)} {...props} /> </CodeBlockContext.Provider> );};export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;export const CodeBlockHeader = ({ className, ...props}: CodeBlockHeaderProps) => ( <div className={cn( 'flex flex-row items-center border-b bg-secondary p-1', className )} {...props} />);export type CodeBlockFilesProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockFiles = ({ className, children, ...props}: CodeBlockFilesProps) => { const { data } = useContext(CodeBlockContext); return ( <div className={cn('flex grow flex-row items-center gap-2', className)} {...props} > {data.map(children)} </div> );};export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & { icon?: IconType; value?: string;};export const CodeBlockFilename = ({ className, icon, value, children, ...props}: CodeBlockFilenameProps) => { const { value: activeValue } = useContext(CodeBlockContext); const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => { const regex = new RegExp( `^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$` ); return regex.test(children as string); })?.[1]; const Icon = icon ?? defaultIcon; if (value !== activeValue) { return null; } return ( <div className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs" {...props} > {Icon && <Icon className="h-4 w-4 shrink-0" />} <span className="flex-1 truncate">{children}</span> </div> );};export type CodeBlockSelectProps = ComponentProps<typeof Select>;export const CodeBlockSelect = (props: CodeBlockSelectProps) => { const { value, onValueChange } = useContext(CodeBlockContext); return <Select onValueChange={onValueChange} value={value} {...props} />;};export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;export const CodeBlockSelectTrigger = ({ className, ...props}: CodeBlockSelectTriggerProps) => ( <SelectTrigger className={cn( 'w-fit border-none text-muted-foreground text-xs shadow-none', className )} {...props} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...props} />);export type CodeBlockSelectContentProps = Omit< ComponentProps<typeof SelectContent>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockSelectContent = ({ children, ...props}: CodeBlockSelectContentProps) => { const { data } = useContext(CodeBlockContext); return <SelectContent {...props}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...props} />);export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};export const CodeBlockCopyButton = ({ asChild, onCopy, onError, timeout = 2000, children, className, ...props}: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const { data, value } = useContext(CodeBlockContext); const code = data.find((item) => item.language === value)?.code; const copyToClipboard = () => { if ( typeof window === 'undefined' || !navigator.clipboard.writeText || !code ) { return; } navigator.clipboard.writeText(code).then(() => { setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); }, onError); }; if (asChild) { return cloneElement(children as ReactElement, { // @ts-expect-error - we know this is a button onClick: copyToClipboard, }); } const Icon = isCopied ? CheckIcon : CopyIcon; return ( <Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...props}> <pre className="w-full"> <code> {children ?.toString() .split('\n') .map((line, i) => ( <span className="line" key={i}> {line} </span> ))} </code> </pre> </div>);export type CodeBlockBodyProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => { const { data } = useContext(CodeBlockContext); return <div {...props}>{data.map(children)}</div>;};export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & { value: string; lineNumbers?: boolean;};export const CodeBlockItem = ({ children, lineNumbers = true, className, value, ...props}: CodeBlockItemProps) => { const { value: activeValue } = useContext(CodeBlockContext); if (value !== activeValue) { return null; } return ( <div className={cn( codeBlockClassName, lineHighlightClassNames, lineDiffClassNames, lineFocusedClassNames, wordHighlightClassNames, darkModeClassNames, lineNumbers && lineNumberClassNames, className )} {...props} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: any; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes, language, syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;};
Highlighted words
Loading component...
'use client';import type { BundledLanguage } from '@/components/ui/shadcn-io/code-block';import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockFilename, CodeBlockFiles, CodeBlockHeader, CodeBlockItem, CodeBlockSelect, CodeBlockSelectContent, CodeBlockSelectItem, CodeBlockSelectTrigger, CodeBlockSelectValue,} from '@/components/ui/shadcn-io/code-block';const code = [ { language: 'jsx', filename: 'MyComponent.jsx', code: `function MyComponent(props) { return ( <div> // [!code word:props.name] <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, }, { language: 'tsx', filename: 'MyComponent.tsx', code: `function MyComponent(props: { name: string }) { return ( <div> // [!code word:props.name] <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, },];const Example = () => ( <CodeBlock data={code} defaultValue={code[0].language}> <CodeBlockHeader> <CodeBlockFiles> {(item) => ( <CodeBlockFilename key={item.language} value={item.language}> {item.filename} </CodeBlockFilename> )} </CodeBlockFiles> <CodeBlockSelect> <CodeBlockSelectTrigger> <CodeBlockSelectValue /> </CodeBlockSelectTrigger> <CodeBlockSelectContent> {(item) => ( <CodeBlockSelectItem key={item.language} value={item.language}> {item.language} </CodeBlockSelectItem> )} </CodeBlockSelectContent> </CodeBlockSelect> <CodeBlockCopyButton onCopy={() => console.log('Copied code to clipboard')} onError={() => console.error('Failed to copy code to clipboard')} /> </CodeBlockHeader> <CodeBlockBody> {(item) => ( <CodeBlockItem key={item.language} value={item.language}> <CodeBlockContent language={item.language as BundledLanguage}> {item.code} </CodeBlockContent> </CodeBlockItem> )} </CodeBlockBody> </CodeBlock>);export default Example;
'use client';import { type IconType, SiAstro, SiBiome, SiBower, SiBun, SiC, SiCircleci, SiCoffeescript, SiCplusplus, SiCss, SiCssmodules, SiDart, SiDocker, SiDocusaurus, SiDotenv, SiEditorconfig, SiEslint, SiGatsby, SiGitignoredotio, SiGnubash, SiGo, SiGraphql, SiGrunt, SiGulp, SiHandlebarsdotjs, SiHtml5, SiJavascript, SiJest, SiJson, SiLess, SiMarkdown, SiMdx, SiMintlify, SiMocha, SiMysql, SiNextdotjs, SiPerl, SiPhp, SiPostcss, SiPrettier, SiPrisma, SiPug, SiPython, SiR, SiReact, SiReadme, SiRedis, SiRemix, SiRive, SiRollupdotjs, SiRuby, SiSanity, SiSass, SiScala, SiSentry, SiShadcnui, SiStorybook, SiStylelint, SiSublimetext, SiSvelte, SiSvg, SiSwift, SiTailwindcss, SiToml, SiTypescript, SiVercel, SiVite, SiVuedotjs, SiWebassembly,} from '@icons-pack/react-simple-icons';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { CheckIcon, CopyIcon } from 'lucide-react';import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode,} from 'react';import { cloneElement, createContext, useContext, useEffect, useState,} from 'react';import { Button } from '@/components/ui/button';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';import { cn } from '@/lib/utils';export type BundledLanguage = string;const filenameIconMap = { '.env': SiDotenv, '*.astro': SiAstro, 'biome.json': SiBiome, '.bowerrc': SiBower, 'bun.lockb': SiBun, '*.c': SiC, '*.cpp': SiCplusplus, '.circleci/config.yml': SiCircleci, '*.coffee': SiCoffeescript, '*.module.css': SiCssmodules, '*.css': SiCss, '*.dart': SiDart, Dockerfile: SiDocker, 'docusaurus.config.js': SiDocusaurus, '.editorconfig': SiEditorconfig, '.eslintrc': SiEslint, 'eslint.config.*': SiEslint, 'gatsby-config.*': SiGatsby, '.gitignore': SiGitignoredotio, '*.go': SiGo, '*.graphql': SiGraphql, '*.sh': SiGnubash, 'Gruntfile.*': SiGrunt, 'gulpfile.*': SiGulp, '*.hbs': SiHandlebarsdotjs, '*.html': SiHtml5, '*.js': SiJavascript, '*.json': SiJson, '*.test.js': SiJest, '*.less': SiLess, '*.md': SiMarkdown, '*.mdx': SiMdx, 'mintlify.json': SiMintlify, 'mocha.opts': SiMocha, '*.mustache': SiHandlebarsdotjs, '*.sql': SiMysql, 'next.config.*': SiNextdotjs, '*.pl': SiPerl, '*.php': SiPhp, 'postcss.config.*': SiPostcss, 'prettier.config.*': SiPrettier, '*.prisma': SiPrisma, '*.pug': SiPug, '*.py': SiPython, '*.r': SiR, '*.rb': SiRuby, '*.jsx': SiReact, '*.tsx': SiReact, 'readme.md': SiReadme, '*.rdb': SiRedis, 'remix.config.*': SiRemix, '*.riv': SiRive, 'rollup.config.*': SiRollupdotjs, 'sanity.config.*': SiSanity, '*.sass': SiSass, '*.scss': SiSass, '*.sc': SiScala, '*.scala': SiScala, 'sentry.client.config.*': SiSentry, 'components.json': SiShadcnui, 'storybook.config.*': SiStorybook, 'stylelint.config.*': SiStylelint, '.sublime-settings': SiSublimetext, '*.svelte': SiSvelte, '*.svg': SiSvg, '*.swift': SiSwift, 'tailwind.config.*': SiTailwindcss, '*.toml': SiToml, '*.ts': SiTypescript, 'vercel.json': SiVercel, 'vite.config.*': SiVite, '*.vue': SiVuedotjs, '*.wasm': SiWebassembly,};const lineNumberClassNames = cn( '[&_code]:[counter-reset:line]', '[&_code]:[counter-increment:line_0]', '[&_.line]:before:content-[counter(line)]', '[&_.line]:before:inline-block', '[&_.line]:before:[counter-increment:line]', '[&_.line]:before:w-4', '[&_.line]:before:mr-4', '[&_.line]:before:text-[13px]', '[&_.line]:before:text-right', '[&_.line]:before:text-muted-foreground/50', '[&_.line]:before:font-mono', '[&_.line]:before:select-none');const darkModeClassNames = cn( 'dark:[&_.shiki]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]', 'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]', 'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]');const lineHighlightClassNames = cn( '[&_.line.highlighted]:bg-blue-50', '[&_.line.highlighted]:after:bg-blue-500', '[&_.line.highlighted]:after:absolute', '[&_.line.highlighted]:after:left-0', '[&_.line.highlighted]:after:top-0', '[&_.line.highlighted]:after:bottom-0', '[&_.line.highlighted]:after:w-0.5', 'dark:[&_.line.highlighted]:!bg-blue-500/10');const lineDiffClassNames = cn( '[&_.line.diff]:after:absolute', '[&_.line.diff]:after:left-0', '[&_.line.diff]:after:top-0', '[&_.line.diff]:after:bottom-0', '[&_.line.diff]:after:w-0.5', '[&_.line.diff.add]:bg-emerald-50', '[&_.line.diff.add]:after:bg-emerald-500', '[&_.line.diff.remove]:bg-rose-50', '[&_.line.diff.remove]:after:bg-rose-500', 'dark:[&_.line.diff.add]:!bg-emerald-500/10', 'dark:[&_.line.diff.remove]:!bg-rose-500/10');const lineFocusedClassNames = cn( '[&_code:has(.focused)_.line]:blur-[2px]', '[&_code:has(.focused)_.line.focused]:blur-none');const wordHighlightClassNames = cn( '[&_.highlighted-word]:bg-blue-50', 'dark:[&_.highlighted-word]:!bg-blue-500/10');const codeBlockClassName = cn( 'mt-0 bg-background text-sm', '[&_pre]:py-4', '[&_.shiki]:!bg-[var(--shiki-bg)]', '[&_code]:w-full', '[&_code]:grid', '[&_code]:overflow-x-auto', '[&_code]:bg-transparent', '[&_.line]:px-4', '[&_.line]:w-full', '[&_.line]:relative');type CodeBlockData = { language: string; filename: string; code: string;};type CodeBlockContextType = { value: string | undefined; onValueChange: ((value: string) => void) | undefined; data: CodeBlockData[];};const CodeBlockContext = createContext<CodeBlockContextType>({ value: undefined, onValueChange: undefined, data: [],});export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; data: CodeBlockData[];};export const CodeBlock = ({ value: controlledValue, onValueChange: controlledOnValueChange, defaultValue, className, data, ...props}: CodeBlockProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); return ( <CodeBlockContext.Provider value={{ value, onValueChange, data }}> <div className={cn('size-full overflow-hidden rounded-md border', className)} {...props} /> </CodeBlockContext.Provider> );};export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;export const CodeBlockHeader = ({ className, ...props}: CodeBlockHeaderProps) => ( <div className={cn( 'flex flex-row items-center border-b bg-secondary p-1', className )} {...props} />);export type CodeBlockFilesProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockFiles = ({ className, children, ...props}: CodeBlockFilesProps) => { const { data } = useContext(CodeBlockContext); return ( <div className={cn('flex grow flex-row items-center gap-2', className)} {...props} > {data.map(children)} </div> );};export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & { icon?: IconType; value?: string;};export const CodeBlockFilename = ({ className, icon, value, children, ...props}: CodeBlockFilenameProps) => { const { value: activeValue } = useContext(CodeBlockContext); const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => { const regex = new RegExp( `^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$` ); return regex.test(children as string); })?.[1]; const Icon = icon ?? defaultIcon; if (value !== activeValue) { return null; } return ( <div className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs" {...props} > {Icon && <Icon className="h-4 w-4 shrink-0" />} <span className="flex-1 truncate">{children}</span> </div> );};export type CodeBlockSelectProps = ComponentProps<typeof Select>;export const CodeBlockSelect = (props: CodeBlockSelectProps) => { const { value, onValueChange } = useContext(CodeBlockContext); return <Select onValueChange={onValueChange} value={value} {...props} />;};export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;export const CodeBlockSelectTrigger = ({ className, ...props}: CodeBlockSelectTriggerProps) => ( <SelectTrigger className={cn( 'w-fit border-none text-muted-foreground text-xs shadow-none', className )} {...props} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...props} />);export type CodeBlockSelectContentProps = Omit< ComponentProps<typeof SelectContent>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockSelectContent = ({ children, ...props}: CodeBlockSelectContentProps) => { const { data } = useContext(CodeBlockContext); return <SelectContent {...props}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...props} />);export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};export const CodeBlockCopyButton = ({ asChild, onCopy, onError, timeout = 2000, children, className, ...props}: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const { data, value } = useContext(CodeBlockContext); const code = data.find((item) => item.language === value)?.code; const copyToClipboard = () => { if ( typeof window === 'undefined' || !navigator.clipboard.writeText || !code ) { return; } navigator.clipboard.writeText(code).then(() => { setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); }, onError); }; if (asChild) { return cloneElement(children as ReactElement, { // @ts-expect-error - we know this is a button onClick: copyToClipboard, }); } const Icon = isCopied ? CheckIcon : CopyIcon; return ( <Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...props}> <pre className="w-full"> <code> {children ?.toString() .split('\n') .map((line, i) => ( <span className="line" key={i}> {line} </span> ))} </code> </pre> </div>);export type CodeBlockBodyProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => { const { data } = useContext(CodeBlockContext); return <div {...props}>{data.map(children)}</div>;};export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & { value: string; lineNumbers?: boolean;};export const CodeBlockItem = ({ children, lineNumbers = true, className, value, ...props}: CodeBlockItemProps) => { const { value: activeValue } = useContext(CodeBlockContext); if (value !== activeValue) { return null; } return ( <div className={cn( codeBlockClassName, lineHighlightClassNames, lineDiffClassNames, lineFocusedClassNames, wordHighlightClassNames, darkModeClassNames, lineNumbers && lineNumberClassNames, className )} {...props} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: any; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes, language, syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;};
Diff
Loading component...
'use client';import type { BundledLanguage } from '@/components/ui/shadcn-io/code-block';import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockFilename, CodeBlockFiles, CodeBlockHeader, CodeBlockItem, CodeBlockSelect, CodeBlockSelectContent, CodeBlockSelectItem, CodeBlockSelectTrigger, CodeBlockSelectValue,} from '@/components/ui/shadcn-io/code-block';const code = [ { language: 'js', filename: 'utils.js', code: `function calculateTotal(items) { let total = 0; for (let i = 0; i < items.length; i++) { total += items[i].price * items[i].quantity; // [!code --] const itemTotal = items[i].price * items[i].quantity; // [!code ++] total += itemTotal; // [!code ++] } return total;}`, }, { language: 'ts', filename: 'utils.ts', code: `interface Item { price: number; quantity: number;}function calculateTotal(items: Item[]): number { let total = 0; for (let i = 0; i < items.length; i++) { total += items[i].price * items[i].quantity; // [!code --] const itemTotal = items[i].price * items[i].quantity; // [!code ++] total += itemTotal; // [!code ++] } return total;}`, },];const Example = () => ( <CodeBlock data={code} defaultValue={code[0].language}> <CodeBlockHeader> <CodeBlockFiles> {(item) => ( <CodeBlockFilename key={item.language} value={item.language}> {item.filename} </CodeBlockFilename> )} </CodeBlockFiles> <CodeBlockSelect> <CodeBlockSelectTrigger> <CodeBlockSelectValue /> </CodeBlockSelectTrigger> <CodeBlockSelectContent> {(item) => ( <CodeBlockSelectItem key={item.language} value={item.language}> {item.language} </CodeBlockSelectItem> )} </CodeBlockSelectContent> </CodeBlockSelect> <CodeBlockCopyButton onCopy={() => console.log('Copied code to clipboard')} onError={() => console.error('Failed to copy code to clipboard')} /> </CodeBlockHeader> <CodeBlockBody> {(item) => ( <CodeBlockItem key={item.language} value={item.language}> <CodeBlockContent language={item.language as BundledLanguage}> {item.code} </CodeBlockContent> </CodeBlockItem> )} </CodeBlockBody> </CodeBlock>);export default Example;
'use client';import { type IconType, SiAstro, SiBiome, SiBower, SiBun, SiC, SiCircleci, SiCoffeescript, SiCplusplus, SiCss, SiCssmodules, SiDart, SiDocker, SiDocusaurus, SiDotenv, SiEditorconfig, SiEslint, SiGatsby, SiGitignoredotio, SiGnubash, SiGo, SiGraphql, SiGrunt, SiGulp, SiHandlebarsdotjs, SiHtml5, SiJavascript, SiJest, SiJson, SiLess, SiMarkdown, SiMdx, SiMintlify, SiMocha, SiMysql, SiNextdotjs, SiPerl, SiPhp, SiPostcss, SiPrettier, SiPrisma, SiPug, SiPython, SiR, SiReact, SiReadme, SiRedis, SiRemix, SiRive, SiRollupdotjs, SiRuby, SiSanity, SiSass, SiScala, SiSentry, SiShadcnui, SiStorybook, SiStylelint, SiSublimetext, SiSvelte, SiSvg, SiSwift, SiTailwindcss, SiToml, SiTypescript, SiVercel, SiVite, SiVuedotjs, SiWebassembly,} from '@icons-pack/react-simple-icons';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { CheckIcon, CopyIcon } from 'lucide-react';import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode,} from 'react';import { cloneElement, createContext, useContext, useEffect, useState,} from 'react';import { Button } from '@/components/ui/button';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';import { cn } from '@/lib/utils';export type BundledLanguage = string;const filenameIconMap = { '.env': SiDotenv, '*.astro': SiAstro, 'biome.json': SiBiome, '.bowerrc': SiBower, 'bun.lockb': SiBun, '*.c': SiC, '*.cpp': SiCplusplus, '.circleci/config.yml': SiCircleci, '*.coffee': SiCoffeescript, '*.module.css': SiCssmodules, '*.css': SiCss, '*.dart': SiDart, Dockerfile: SiDocker, 'docusaurus.config.js': SiDocusaurus, '.editorconfig': SiEditorconfig, '.eslintrc': SiEslint, 'eslint.config.*': SiEslint, 'gatsby-config.*': SiGatsby, '.gitignore': SiGitignoredotio, '*.go': SiGo, '*.graphql': SiGraphql, '*.sh': SiGnubash, 'Gruntfile.*': SiGrunt, 'gulpfile.*': SiGulp, '*.hbs': SiHandlebarsdotjs, '*.html': SiHtml5, '*.js': SiJavascript, '*.json': SiJson, '*.test.js': SiJest, '*.less': SiLess, '*.md': SiMarkdown, '*.mdx': SiMdx, 'mintlify.json': SiMintlify, 'mocha.opts': SiMocha, '*.mustache': SiHandlebarsdotjs, '*.sql': SiMysql, 'next.config.*': SiNextdotjs, '*.pl': SiPerl, '*.php': SiPhp, 'postcss.config.*': SiPostcss, 'prettier.config.*': SiPrettier, '*.prisma': SiPrisma, '*.pug': SiPug, '*.py': SiPython, '*.r': SiR, '*.rb': SiRuby, '*.jsx': SiReact, '*.tsx': SiReact, 'readme.md': SiReadme, '*.rdb': SiRedis, 'remix.config.*': SiRemix, '*.riv': SiRive, 'rollup.config.*': SiRollupdotjs, 'sanity.config.*': SiSanity, '*.sass': SiSass, '*.scss': SiSass, '*.sc': SiScala, '*.scala': SiScala, 'sentry.client.config.*': SiSentry, 'components.json': SiShadcnui, 'storybook.config.*': SiStorybook, 'stylelint.config.*': SiStylelint, '.sublime-settings': SiSublimetext, '*.svelte': SiSvelte, '*.svg': SiSvg, '*.swift': SiSwift, 'tailwind.config.*': SiTailwindcss, '*.toml': SiToml, '*.ts': SiTypescript, 'vercel.json': SiVercel, 'vite.config.*': SiVite, '*.vue': SiVuedotjs, '*.wasm': SiWebassembly,};const lineNumberClassNames = cn( '[&_code]:[counter-reset:line]', '[&_code]:[counter-increment:line_0]', '[&_.line]:before:content-[counter(line)]', '[&_.line]:before:inline-block', '[&_.line]:before:[counter-increment:line]', '[&_.line]:before:w-4', '[&_.line]:before:mr-4', '[&_.line]:before:text-[13px]', '[&_.line]:before:text-right', '[&_.line]:before:text-muted-foreground/50', '[&_.line]:before:font-mono', '[&_.line]:before:select-none');const darkModeClassNames = cn( 'dark:[&_.shiki]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]', 'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]', 'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]');const lineHighlightClassNames = cn( '[&_.line.highlighted]:bg-blue-50', '[&_.line.highlighted]:after:bg-blue-500', '[&_.line.highlighted]:after:absolute', '[&_.line.highlighted]:after:left-0', '[&_.line.highlighted]:after:top-0', '[&_.line.highlighted]:after:bottom-0', '[&_.line.highlighted]:after:w-0.5', 'dark:[&_.line.highlighted]:!bg-blue-500/10');const lineDiffClassNames = cn( '[&_.line.diff]:after:absolute', '[&_.line.diff]:after:left-0', '[&_.line.diff]:after:top-0', '[&_.line.diff]:after:bottom-0', '[&_.line.diff]:after:w-0.5', '[&_.line.diff.add]:bg-emerald-50', '[&_.line.diff.add]:after:bg-emerald-500', '[&_.line.diff.remove]:bg-rose-50', '[&_.line.diff.remove]:after:bg-rose-500', 'dark:[&_.line.diff.add]:!bg-emerald-500/10', 'dark:[&_.line.diff.remove]:!bg-rose-500/10');const lineFocusedClassNames = cn( '[&_code:has(.focused)_.line]:blur-[2px]', '[&_code:has(.focused)_.line.focused]:blur-none');const wordHighlightClassNames = cn( '[&_.highlighted-word]:bg-blue-50', 'dark:[&_.highlighted-word]:!bg-blue-500/10');const codeBlockClassName = cn( 'mt-0 bg-background text-sm', '[&_pre]:py-4', '[&_.shiki]:!bg-[var(--shiki-bg)]', '[&_code]:w-full', '[&_code]:grid', '[&_code]:overflow-x-auto', '[&_code]:bg-transparent', '[&_.line]:px-4', '[&_.line]:w-full', '[&_.line]:relative');type CodeBlockData = { language: string; filename: string; code: string;};type CodeBlockContextType = { value: string | undefined; onValueChange: ((value: string) => void) | undefined; data: CodeBlockData[];};const CodeBlockContext = createContext<CodeBlockContextType>({ value: undefined, onValueChange: undefined, data: [],});export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; data: CodeBlockData[];};export const CodeBlock = ({ value: controlledValue, onValueChange: controlledOnValueChange, defaultValue, className, data, ...props}: CodeBlockProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); return ( <CodeBlockContext.Provider value={{ value, onValueChange, data }}> <div className={cn('size-full overflow-hidden rounded-md border', className)} {...props} /> </CodeBlockContext.Provider> );};export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;export const CodeBlockHeader = ({ className, ...props}: CodeBlockHeaderProps) => ( <div className={cn( 'flex flex-row items-center border-b bg-secondary p-1', className )} {...props} />);export type CodeBlockFilesProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockFiles = ({ className, children, ...props}: CodeBlockFilesProps) => { const { data } = useContext(CodeBlockContext); return ( <div className={cn('flex grow flex-row items-center gap-2', className)} {...props} > {data.map(children)} </div> );};export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & { icon?: IconType; value?: string;};export const CodeBlockFilename = ({ className, icon, value, children, ...props}: CodeBlockFilenameProps) => { const { value: activeValue } = useContext(CodeBlockContext); const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => { const regex = new RegExp( `^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$` ); return regex.test(children as string); })?.[1]; const Icon = icon ?? defaultIcon; if (value !== activeValue) { return null; } return ( <div className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs" {...props} > {Icon && <Icon className="h-4 w-4 shrink-0" />} <span className="flex-1 truncate">{children}</span> </div> );};export type CodeBlockSelectProps = ComponentProps<typeof Select>;export const CodeBlockSelect = (props: CodeBlockSelectProps) => { const { value, onValueChange } = useContext(CodeBlockContext); return <Select onValueChange={onValueChange} value={value} {...props} />;};export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;export const CodeBlockSelectTrigger = ({ className, ...props}: CodeBlockSelectTriggerProps) => ( <SelectTrigger className={cn( 'w-fit border-none text-muted-foreground text-xs shadow-none', className )} {...props} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...props} />);export type CodeBlockSelectContentProps = Omit< ComponentProps<typeof SelectContent>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockSelectContent = ({ children, ...props}: CodeBlockSelectContentProps) => { const { data } = useContext(CodeBlockContext); return <SelectContent {...props}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...props} />);export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};export const CodeBlockCopyButton = ({ asChild, onCopy, onError, timeout = 2000, children, className, ...props}: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const { data, value } = useContext(CodeBlockContext); const code = data.find((item) => item.language === value)?.code; const copyToClipboard = () => { if ( typeof window === 'undefined' || !navigator.clipboard.writeText || !code ) { return; } navigator.clipboard.writeText(code).then(() => { setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); }, onError); }; if (asChild) { return cloneElement(children as ReactElement, { // @ts-expect-error - we know this is a button onClick: copyToClipboard, }); } const Icon = isCopied ? CheckIcon : CopyIcon; return ( <Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...props}> <pre className="w-full"> <code> {children ?.toString() .split('\n') .map((line, i) => ( <span className="line" key={i}> {line} </span> ))} </code> </pre> </div>);export type CodeBlockBodyProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => { const { data } = useContext(CodeBlockContext); return <div {...props}>{data.map(children)}</div>;};export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & { value: string; lineNumbers?: boolean;};export const CodeBlockItem = ({ children, lineNumbers = true, className, value, ...props}: CodeBlockItemProps) => { const { value: activeValue } = useContext(CodeBlockContext); if (value !== activeValue) { return null; } return ( <div className={cn( codeBlockClassName, lineHighlightClassNames, lineDiffClassNames, lineFocusedClassNames, wordHighlightClassNames, darkModeClassNames, lineNumbers && lineNumberClassNames, className )} {...props} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: any; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes, language, syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;};
Focus
Loading component...
'use client';import type { BundledLanguage } from '@/components/ui/shadcn-io/code-block';import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockFilename, CodeBlockFiles, CodeBlockHeader, CodeBlockItem, CodeBlockSelect, CodeBlockSelectContent, CodeBlockSelectItem, CodeBlockSelectTrigger, CodeBlockSelectValue,} from '@/components/ui/shadcn-io/code-block';const code = [ { language: 'js', filename: 'utils.js', code: `function calculateDiscount(price, percentage) { const discount = price * (percentage / 100); // [!code focus] return price - discount;}// Example usageconst finalPrice = calculateDiscount(100, 20);console.log(finalPrice); // 80`, }, { language: 'ts', filename: 'utils.ts', code: `function calculateDiscount(price: number, percentage: number): number { const discount = price * (percentage / 100); // [!code focus] return price - discount;}// Example usageconst finalPrice: number = calculateDiscount(100, 20);console.log(finalPrice); // 80`, },];const Example = () => ( <CodeBlock data={code} defaultValue={code[0].language}> <CodeBlockHeader> <CodeBlockFiles> {(item) => ( <CodeBlockFilename key={item.language} value={item.language}> {item.filename} </CodeBlockFilename> )} </CodeBlockFiles> <CodeBlockSelect> <CodeBlockSelectTrigger> <CodeBlockSelectValue /> </CodeBlockSelectTrigger> <CodeBlockSelectContent> {(item) => ( <CodeBlockSelectItem key={item.language} value={item.language}> {item.language} </CodeBlockSelectItem> )} </CodeBlockSelectContent> </CodeBlockSelect> <CodeBlockCopyButton onCopy={() => console.log('Copied code to clipboard')} onError={() => console.error('Failed to copy code to clipboard')} /> </CodeBlockHeader> <CodeBlockBody> {(item) => ( <CodeBlockItem key={item.language} value={item.language}> <CodeBlockContent language={item.language as BundledLanguage}> {item.code} </CodeBlockContent> </CodeBlockItem> )} </CodeBlockBody> </CodeBlock>);export default Example;
'use client';import { type IconType, SiAstro, SiBiome, SiBower, SiBun, SiC, SiCircleci, SiCoffeescript, SiCplusplus, SiCss, SiCssmodules, SiDart, SiDocker, SiDocusaurus, SiDotenv, SiEditorconfig, SiEslint, SiGatsby, SiGitignoredotio, SiGnubash, SiGo, SiGraphql, SiGrunt, SiGulp, SiHandlebarsdotjs, SiHtml5, SiJavascript, SiJest, SiJson, SiLess, SiMarkdown, SiMdx, SiMintlify, SiMocha, SiMysql, SiNextdotjs, SiPerl, SiPhp, SiPostcss, SiPrettier, SiPrisma, SiPug, SiPython, SiR, SiReact, SiReadme, SiRedis, SiRemix, SiRive, SiRollupdotjs, SiRuby, SiSanity, SiSass, SiScala, SiSentry, SiShadcnui, SiStorybook, SiStylelint, SiSublimetext, SiSvelte, SiSvg, SiSwift, SiTailwindcss, SiToml, SiTypescript, SiVercel, SiVite, SiVuedotjs, SiWebassembly,} from '@icons-pack/react-simple-icons';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { CheckIcon, CopyIcon } from 'lucide-react';import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode,} from 'react';import { cloneElement, createContext, useContext, useEffect, useState,} from 'react';import { Button } from '@/components/ui/button';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';import { cn } from '@/lib/utils';export type BundledLanguage = string;const filenameIconMap = { '.env': SiDotenv, '*.astro': SiAstro, 'biome.json': SiBiome, '.bowerrc': SiBower, 'bun.lockb': SiBun, '*.c': SiC, '*.cpp': SiCplusplus, '.circleci/config.yml': SiCircleci, '*.coffee': SiCoffeescript, '*.module.css': SiCssmodules, '*.css': SiCss, '*.dart': SiDart, Dockerfile: SiDocker, 'docusaurus.config.js': SiDocusaurus, '.editorconfig': SiEditorconfig, '.eslintrc': SiEslint, 'eslint.config.*': SiEslint, 'gatsby-config.*': SiGatsby, '.gitignore': SiGitignoredotio, '*.go': SiGo, '*.graphql': SiGraphql, '*.sh': SiGnubash, 'Gruntfile.*': SiGrunt, 'gulpfile.*': SiGulp, '*.hbs': SiHandlebarsdotjs, '*.html': SiHtml5, '*.js': SiJavascript, '*.json': SiJson, '*.test.js': SiJest, '*.less': SiLess, '*.md': SiMarkdown, '*.mdx': SiMdx, 'mintlify.json': SiMintlify, 'mocha.opts': SiMocha, '*.mustache': SiHandlebarsdotjs, '*.sql': SiMysql, 'next.config.*': SiNextdotjs, '*.pl': SiPerl, '*.php': SiPhp, 'postcss.config.*': SiPostcss, 'prettier.config.*': SiPrettier, '*.prisma': SiPrisma, '*.pug': SiPug, '*.py': SiPython, '*.r': SiR, '*.rb': SiRuby, '*.jsx': SiReact, '*.tsx': SiReact, 'readme.md': SiReadme, '*.rdb': SiRedis, 'remix.config.*': SiRemix, '*.riv': SiRive, 'rollup.config.*': SiRollupdotjs, 'sanity.config.*': SiSanity, '*.sass': SiSass, '*.scss': SiSass, '*.sc': SiScala, '*.scala': SiScala, 'sentry.client.config.*': SiSentry, 'components.json': SiShadcnui, 'storybook.config.*': SiStorybook, 'stylelint.config.*': SiStylelint, '.sublime-settings': SiSublimetext, '*.svelte': SiSvelte, '*.svg': SiSvg, '*.swift': SiSwift, 'tailwind.config.*': SiTailwindcss, '*.toml': SiToml, '*.ts': SiTypescript, 'vercel.json': SiVercel, 'vite.config.*': SiVite, '*.vue': SiVuedotjs, '*.wasm': SiWebassembly,};const lineNumberClassNames = cn( '[&_code]:[counter-reset:line]', '[&_code]:[counter-increment:line_0]', '[&_.line]:before:content-[counter(line)]', '[&_.line]:before:inline-block', '[&_.line]:before:[counter-increment:line]', '[&_.line]:before:w-4', '[&_.line]:before:mr-4', '[&_.line]:before:text-[13px]', '[&_.line]:before:text-right', '[&_.line]:before:text-muted-foreground/50', '[&_.line]:before:font-mono', '[&_.line]:before:select-none');const darkModeClassNames = cn( 'dark:[&_.shiki]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]', 'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]', 'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]');const lineHighlightClassNames = cn( '[&_.line.highlighted]:bg-blue-50', '[&_.line.highlighted]:after:bg-blue-500', '[&_.line.highlighted]:after:absolute', '[&_.line.highlighted]:after:left-0', '[&_.line.highlighted]:after:top-0', '[&_.line.highlighted]:after:bottom-0', '[&_.line.highlighted]:after:w-0.5', 'dark:[&_.line.highlighted]:!bg-blue-500/10');const lineDiffClassNames = cn( '[&_.line.diff]:after:absolute', '[&_.line.diff]:after:left-0', '[&_.line.diff]:after:top-0', '[&_.line.diff]:after:bottom-0', '[&_.line.diff]:after:w-0.5', '[&_.line.diff.add]:bg-emerald-50', '[&_.line.diff.add]:after:bg-emerald-500', '[&_.line.diff.remove]:bg-rose-50', '[&_.line.diff.remove]:after:bg-rose-500', 'dark:[&_.line.diff.add]:!bg-emerald-500/10', 'dark:[&_.line.diff.remove]:!bg-rose-500/10');const lineFocusedClassNames = cn( '[&_code:has(.focused)_.line]:blur-[2px]', '[&_code:has(.focused)_.line.focused]:blur-none');const wordHighlightClassNames = cn( '[&_.highlighted-word]:bg-blue-50', 'dark:[&_.highlighted-word]:!bg-blue-500/10');const codeBlockClassName = cn( 'mt-0 bg-background text-sm', '[&_pre]:py-4', '[&_.shiki]:!bg-[var(--shiki-bg)]', '[&_code]:w-full', '[&_code]:grid', '[&_code]:overflow-x-auto', '[&_code]:bg-transparent', '[&_.line]:px-4', '[&_.line]:w-full', '[&_.line]:relative');type CodeBlockData = { language: string; filename: string; code: string;};type CodeBlockContextType = { value: string | undefined; onValueChange: ((value: string) => void) | undefined; data: CodeBlockData[];};const CodeBlockContext = createContext<CodeBlockContextType>({ value: undefined, onValueChange: undefined, data: [],});export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; data: CodeBlockData[];};export const CodeBlock = ({ value: controlledValue, onValueChange: controlledOnValueChange, defaultValue, className, data, ...props}: CodeBlockProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); return ( <CodeBlockContext.Provider value={{ value, onValueChange, data }}> <div className={cn('size-full overflow-hidden rounded-md border', className)} {...props} /> </CodeBlockContext.Provider> );};export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;export const CodeBlockHeader = ({ className, ...props}: CodeBlockHeaderProps) => ( <div className={cn( 'flex flex-row items-center border-b bg-secondary p-1', className )} {...props} />);export type CodeBlockFilesProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockFiles = ({ className, children, ...props}: CodeBlockFilesProps) => { const { data } = useContext(CodeBlockContext); return ( <div className={cn('flex grow flex-row items-center gap-2', className)} {...props} > {data.map(children)} </div> );};export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & { icon?: IconType; value?: string;};export const CodeBlockFilename = ({ className, icon, value, children, ...props}: CodeBlockFilenameProps) => { const { value: activeValue } = useContext(CodeBlockContext); const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => { const regex = new RegExp( `^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$` ); return regex.test(children as string); })?.[1]; const Icon = icon ?? defaultIcon; if (value !== activeValue) { return null; } return ( <div className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs" {...props} > {Icon && <Icon className="h-4 w-4 shrink-0" />} <span className="flex-1 truncate">{children}</span> </div> );};export type CodeBlockSelectProps = ComponentProps<typeof Select>;export const CodeBlockSelect = (props: CodeBlockSelectProps) => { const { value, onValueChange } = useContext(CodeBlockContext); return <Select onValueChange={onValueChange} value={value} {...props} />;};export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;export const CodeBlockSelectTrigger = ({ className, ...props}: CodeBlockSelectTriggerProps) => ( <SelectTrigger className={cn( 'w-fit border-none text-muted-foreground text-xs shadow-none', className )} {...props} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...props} />);export type CodeBlockSelectContentProps = Omit< ComponentProps<typeof SelectContent>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockSelectContent = ({ children, ...props}: CodeBlockSelectContentProps) => { const { data } = useContext(CodeBlockContext); return <SelectContent {...props}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...props} />);export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};export const CodeBlockCopyButton = ({ asChild, onCopy, onError, timeout = 2000, children, className, ...props}: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const { data, value } = useContext(CodeBlockContext); const code = data.find((item) => item.language === value)?.code; const copyToClipboard = () => { if ( typeof window === 'undefined' || !navigator.clipboard.writeText || !code ) { return; } navigator.clipboard.writeText(code).then(() => { setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); }, onError); }; if (asChild) { return cloneElement(children as ReactElement, { // @ts-expect-error - we know this is a button onClick: copyToClipboard, }); } const Icon = isCopied ? CheckIcon : CopyIcon; return ( <Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...props}> <pre className="w-full"> <code> {children ?.toString() .split('\n') .map((line, i) => ( <span className="line" key={i}> {line} </span> ))} </code> </pre> </div>);export type CodeBlockBodyProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => { const { data } = useContext(CodeBlockContext); return <div {...props}>{data.map(children)}</div>;};export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & { value: string; lineNumbers?: boolean;};export const CodeBlockItem = ({ children, lineNumbers = true, className, value, ...props}: CodeBlockItemProps) => { const { value: activeValue } = useContext(CodeBlockContext); if (value !== activeValue) { return null; } return ( <div className={cn( codeBlockClassName, lineHighlightClassNames, lineDiffClassNames, lineFocusedClassNames, wordHighlightClassNames, darkModeClassNames, lineNumbers && lineNumberClassNames, className )} {...props} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: any; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes, language, syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;};
Hidden line numbers
Loading component...
'use client';import type { BundledLanguage } from '@/components/ui/shadcn-io/code-block';import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockFilename, CodeBlockFiles, CodeBlockHeader, CodeBlockItem, CodeBlockSelect, CodeBlockSelectContent, CodeBlockSelectItem, CodeBlockSelectTrigger, CodeBlockSelectValue,} from '@/components/ui/shadcn-io/code-block';const code = [ { language: 'jsx', filename: 'MyComponent.jsx', code: `function MyComponent(props) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, }, { language: 'tsx', filename: 'MyComponent.tsx', code: `function MyComponent(props: { name: string }) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, },];const Example = () => ( <CodeBlock data={code} defaultValue={code[0].language}> <CodeBlockHeader> <CodeBlockFiles> {(item) => ( <CodeBlockFilename key={item.language} value={item.language}> {item.filename} </CodeBlockFilename> )} </CodeBlockFiles> <CodeBlockSelect> <CodeBlockSelectTrigger> <CodeBlockSelectValue /> </CodeBlockSelectTrigger> <CodeBlockSelectContent> {(item) => ( <CodeBlockSelectItem key={item.language} value={item.language}> {item.language} </CodeBlockSelectItem> )} </CodeBlockSelectContent> </CodeBlockSelect> <CodeBlockCopyButton onCopy={() => console.log('Copied code to clipboard')} onError={() => console.error('Failed to copy code to clipboard')} /> </CodeBlockHeader> <CodeBlockBody> {(item) => ( <CodeBlockItem key={item.language} lineNumbers={false} value={item.language} > <CodeBlockContent language={item.language as BundledLanguage}> {item.code} </CodeBlockContent> </CodeBlockItem> )} </CodeBlockBody> </CodeBlock>);export default Example;
'use client';import { type IconType, SiAstro, SiBiome, SiBower, SiBun, SiC, SiCircleci, SiCoffeescript, SiCplusplus, SiCss, SiCssmodules, SiDart, SiDocker, SiDocusaurus, SiDotenv, SiEditorconfig, SiEslint, SiGatsby, SiGitignoredotio, SiGnubash, SiGo, SiGraphql, SiGrunt, SiGulp, SiHandlebarsdotjs, SiHtml5, SiJavascript, SiJest, SiJson, SiLess, SiMarkdown, SiMdx, SiMintlify, SiMocha, SiMysql, SiNextdotjs, SiPerl, SiPhp, SiPostcss, SiPrettier, SiPrisma, SiPug, SiPython, SiR, SiReact, SiReadme, SiRedis, SiRemix, SiRive, SiRollupdotjs, SiRuby, SiSanity, SiSass, SiScala, SiSentry, SiShadcnui, SiStorybook, SiStylelint, SiSublimetext, SiSvelte, SiSvg, SiSwift, SiTailwindcss, SiToml, SiTypescript, SiVercel, SiVite, SiVuedotjs, SiWebassembly,} from '@icons-pack/react-simple-icons';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { CheckIcon, CopyIcon } from 'lucide-react';import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode,} from 'react';import { cloneElement, createContext, useContext, useEffect, useState,} from 'react';import { Button } from '@/components/ui/button';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';import { cn } from '@/lib/utils';export type BundledLanguage = string;const filenameIconMap = { '.env': SiDotenv, '*.astro': SiAstro, 'biome.json': SiBiome, '.bowerrc': SiBower, 'bun.lockb': SiBun, '*.c': SiC, '*.cpp': SiCplusplus, '.circleci/config.yml': SiCircleci, '*.coffee': SiCoffeescript, '*.module.css': SiCssmodules, '*.css': SiCss, '*.dart': SiDart, Dockerfile: SiDocker, 'docusaurus.config.js': SiDocusaurus, '.editorconfig': SiEditorconfig, '.eslintrc': SiEslint, 'eslint.config.*': SiEslint, 'gatsby-config.*': SiGatsby, '.gitignore': SiGitignoredotio, '*.go': SiGo, '*.graphql': SiGraphql, '*.sh': SiGnubash, 'Gruntfile.*': SiGrunt, 'gulpfile.*': SiGulp, '*.hbs': SiHandlebarsdotjs, '*.html': SiHtml5, '*.js': SiJavascript, '*.json': SiJson, '*.test.js': SiJest, '*.less': SiLess, '*.md': SiMarkdown, '*.mdx': SiMdx, 'mintlify.json': SiMintlify, 'mocha.opts': SiMocha, '*.mustache': SiHandlebarsdotjs, '*.sql': SiMysql, 'next.config.*': SiNextdotjs, '*.pl': SiPerl, '*.php': SiPhp, 'postcss.config.*': SiPostcss, 'prettier.config.*': SiPrettier, '*.prisma': SiPrisma, '*.pug': SiPug, '*.py': SiPython, '*.r': SiR, '*.rb': SiRuby, '*.jsx': SiReact, '*.tsx': SiReact, 'readme.md': SiReadme, '*.rdb': SiRedis, 'remix.config.*': SiRemix, '*.riv': SiRive, 'rollup.config.*': SiRollupdotjs, 'sanity.config.*': SiSanity, '*.sass': SiSass, '*.scss': SiSass, '*.sc': SiScala, '*.scala': SiScala, 'sentry.client.config.*': SiSentry, 'components.json': SiShadcnui, 'storybook.config.*': SiStorybook, 'stylelint.config.*': SiStylelint, '.sublime-settings': SiSublimetext, '*.svelte': SiSvelte, '*.svg': SiSvg, '*.swift': SiSwift, 'tailwind.config.*': SiTailwindcss, '*.toml': SiToml, '*.ts': SiTypescript, 'vercel.json': SiVercel, 'vite.config.*': SiVite, '*.vue': SiVuedotjs, '*.wasm': SiWebassembly,};const lineNumberClassNames = cn( '[&_code]:[counter-reset:line]', '[&_code]:[counter-increment:line_0]', '[&_.line]:before:content-[counter(line)]', '[&_.line]:before:inline-block', '[&_.line]:before:[counter-increment:line]', '[&_.line]:before:w-4', '[&_.line]:before:mr-4', '[&_.line]:before:text-[13px]', '[&_.line]:before:text-right', '[&_.line]:before:text-muted-foreground/50', '[&_.line]:before:font-mono', '[&_.line]:before:select-none');const darkModeClassNames = cn( 'dark:[&_.shiki]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]', 'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]', 'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]');const lineHighlightClassNames = cn( '[&_.line.highlighted]:bg-blue-50', '[&_.line.highlighted]:after:bg-blue-500', '[&_.line.highlighted]:after:absolute', '[&_.line.highlighted]:after:left-0', '[&_.line.highlighted]:after:top-0', '[&_.line.highlighted]:after:bottom-0', '[&_.line.highlighted]:after:w-0.5', 'dark:[&_.line.highlighted]:!bg-blue-500/10');const lineDiffClassNames = cn( '[&_.line.diff]:after:absolute', '[&_.line.diff]:after:left-0', '[&_.line.diff]:after:top-0', '[&_.line.diff]:after:bottom-0', '[&_.line.diff]:after:w-0.5', '[&_.line.diff.add]:bg-emerald-50', '[&_.line.diff.add]:after:bg-emerald-500', '[&_.line.diff.remove]:bg-rose-50', '[&_.line.diff.remove]:after:bg-rose-500', 'dark:[&_.line.diff.add]:!bg-emerald-500/10', 'dark:[&_.line.diff.remove]:!bg-rose-500/10');const lineFocusedClassNames = cn( '[&_code:has(.focused)_.line]:blur-[2px]', '[&_code:has(.focused)_.line.focused]:blur-none');const wordHighlightClassNames = cn( '[&_.highlighted-word]:bg-blue-50', 'dark:[&_.highlighted-word]:!bg-blue-500/10');const codeBlockClassName = cn( 'mt-0 bg-background text-sm', '[&_pre]:py-4', '[&_.shiki]:!bg-[var(--shiki-bg)]', '[&_code]:w-full', '[&_code]:grid', '[&_code]:overflow-x-auto', '[&_code]:bg-transparent', '[&_.line]:px-4', '[&_.line]:w-full', '[&_.line]:relative');type CodeBlockData = { language: string; filename: string; code: string;};type CodeBlockContextType = { value: string | undefined; onValueChange: ((value: string) => void) | undefined; data: CodeBlockData[];};const CodeBlockContext = createContext<CodeBlockContextType>({ value: undefined, onValueChange: undefined, data: [],});export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; data: CodeBlockData[];};export const CodeBlock = ({ value: controlledValue, onValueChange: controlledOnValueChange, defaultValue, className, data, ...props}: CodeBlockProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); return ( <CodeBlockContext.Provider value={{ value, onValueChange, data }}> <div className={cn('size-full overflow-hidden rounded-md border', className)} {...props} /> </CodeBlockContext.Provider> );};export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;export const CodeBlockHeader = ({ className, ...props}: CodeBlockHeaderProps) => ( <div className={cn( 'flex flex-row items-center border-b bg-secondary p-1', className )} {...props} />);export type CodeBlockFilesProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockFiles = ({ className, children, ...props}: CodeBlockFilesProps) => { const { data } = useContext(CodeBlockContext); return ( <div className={cn('flex grow flex-row items-center gap-2', className)} {...props} > {data.map(children)} </div> );};export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & { icon?: IconType; value?: string;};export const CodeBlockFilename = ({ className, icon, value, children, ...props}: CodeBlockFilenameProps) => { const { value: activeValue } = useContext(CodeBlockContext); const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => { const regex = new RegExp( `^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$` ); return regex.test(children as string); })?.[1]; const Icon = icon ?? defaultIcon; if (value !== activeValue) { return null; } return ( <div className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs" {...props} > {Icon && <Icon className="h-4 w-4 shrink-0" />} <span className="flex-1 truncate">{children}</span> </div> );};export type CodeBlockSelectProps = ComponentProps<typeof Select>;export const CodeBlockSelect = (props: CodeBlockSelectProps) => { const { value, onValueChange } = useContext(CodeBlockContext); return <Select onValueChange={onValueChange} value={value} {...props} />;};export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;export const CodeBlockSelectTrigger = ({ className, ...props}: CodeBlockSelectTriggerProps) => ( <SelectTrigger className={cn( 'w-fit border-none text-muted-foreground text-xs shadow-none', className )} {...props} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...props} />);export type CodeBlockSelectContentProps = Omit< ComponentProps<typeof SelectContent>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockSelectContent = ({ children, ...props}: CodeBlockSelectContentProps) => { const { data } = useContext(CodeBlockContext); return <SelectContent {...props}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...props} />);export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};export const CodeBlockCopyButton = ({ asChild, onCopy, onError, timeout = 2000, children, className, ...props}: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const { data, value } = useContext(CodeBlockContext); const code = data.find((item) => item.language === value)?.code; const copyToClipboard = () => { if ( typeof window === 'undefined' || !navigator.clipboard.writeText || !code ) { return; } navigator.clipboard.writeText(code).then(() => { setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); }, onError); }; if (asChild) { return cloneElement(children as ReactElement, { // @ts-expect-error - we know this is a button onClick: copyToClipboard, }); } const Icon = isCopied ? CheckIcon : CopyIcon; return ( <Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...props}> <pre className="w-full"> <code> {children ?.toString() .split('\n') .map((line, i) => ( <span className="line" key={i}> {line} </span> ))} </code> </pre> </div>);export type CodeBlockBodyProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => { const { data } = useContext(CodeBlockContext); return <div {...props}>{data.map(children)}</div>;};export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & { value: string; lineNumbers?: boolean;};export const CodeBlockItem = ({ children, lineNumbers = true, className, value, ...props}: CodeBlockItemProps) => { const { value: activeValue } = useContext(CodeBlockContext); if (value !== activeValue) { return null; } return ( <div className={cn( codeBlockClassName, lineHighlightClassNames, lineDiffClassNames, lineFocusedClassNames, wordHighlightClassNames, darkModeClassNames, lineNumbers && lineNumberClassNames, className )} {...props} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: any; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes, language, syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;};
No syntax highlighting
Loading component...
'use client';import type { BundledLanguage } from '@/components/ui/shadcn-io/code-block';import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockFilename, CodeBlockFiles, CodeBlockHeader, CodeBlockItem, CodeBlockSelect, CodeBlockSelectContent, CodeBlockSelectItem, CodeBlockSelectTrigger, CodeBlockSelectValue,} from '@/components/ui/shadcn-io/code-block';const code = [ { language: 'jsx', filename: 'MyComponent.jsx', code: `function MyComponent(props) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, }, { language: 'tsx', filename: 'MyComponent.tsx', code: `function MyComponent(props: { name: string }) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, },];const Example = () => ( <CodeBlock data={code} defaultValue={code[0].language}> <CodeBlockHeader> <CodeBlockFiles> {(item) => ( <CodeBlockFilename key={item.language} value={item.language}> {item.filename} </CodeBlockFilename> )} </CodeBlockFiles> <CodeBlockSelect> <CodeBlockSelectTrigger> <CodeBlockSelectValue /> </CodeBlockSelectTrigger> <CodeBlockSelectContent> {(item) => ( <CodeBlockSelectItem key={item.language} value={item.language}> {item.language} </CodeBlockSelectItem> )} </CodeBlockSelectContent> </CodeBlockSelect> <CodeBlockCopyButton onCopy={() => console.log('Copied code to clipboard')} onError={() => console.error('Failed to copy code to clipboard')} /> </CodeBlockHeader> <CodeBlockBody> {(item) => ( <CodeBlockItem key={item.language} value={item.language}> <CodeBlockContent language={item.language as BundledLanguage} syntaxHighlighting={false} > {item.code} </CodeBlockContent> </CodeBlockItem> )} </CodeBlockBody> </CodeBlock>);export default Example;
'use client';import { type IconType, SiAstro, SiBiome, SiBower, SiBun, SiC, SiCircleci, SiCoffeescript, SiCplusplus, SiCss, SiCssmodules, SiDart, SiDocker, SiDocusaurus, SiDotenv, SiEditorconfig, SiEslint, SiGatsby, SiGitignoredotio, SiGnubash, SiGo, SiGraphql, SiGrunt, SiGulp, SiHandlebarsdotjs, SiHtml5, SiJavascript, SiJest, SiJson, SiLess, SiMarkdown, SiMdx, SiMintlify, SiMocha, SiMysql, SiNextdotjs, SiPerl, SiPhp, SiPostcss, SiPrettier, SiPrisma, SiPug, SiPython, SiR, SiReact, SiReadme, SiRedis, SiRemix, SiRive, SiRollupdotjs, SiRuby, SiSanity, SiSass, SiScala, SiSentry, SiShadcnui, SiStorybook, SiStylelint, SiSublimetext, SiSvelte, SiSvg, SiSwift, SiTailwindcss, SiToml, SiTypescript, SiVercel, SiVite, SiVuedotjs, SiWebassembly,} from '@icons-pack/react-simple-icons';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { CheckIcon, CopyIcon } from 'lucide-react';import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode,} from 'react';import { cloneElement, createContext, useContext, useEffect, useState,} from 'react';import { Button } from '@/components/ui/button';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';import { cn } from '@/lib/utils';export type BundledLanguage = string;const filenameIconMap = { '.env': SiDotenv, '*.astro': SiAstro, 'biome.json': SiBiome, '.bowerrc': SiBower, 'bun.lockb': SiBun, '*.c': SiC, '*.cpp': SiCplusplus, '.circleci/config.yml': SiCircleci, '*.coffee': SiCoffeescript, '*.module.css': SiCssmodules, '*.css': SiCss, '*.dart': SiDart, Dockerfile: SiDocker, 'docusaurus.config.js': SiDocusaurus, '.editorconfig': SiEditorconfig, '.eslintrc': SiEslint, 'eslint.config.*': SiEslint, 'gatsby-config.*': SiGatsby, '.gitignore': SiGitignoredotio, '*.go': SiGo, '*.graphql': SiGraphql, '*.sh': SiGnubash, 'Gruntfile.*': SiGrunt, 'gulpfile.*': SiGulp, '*.hbs': SiHandlebarsdotjs, '*.html': SiHtml5, '*.js': SiJavascript, '*.json': SiJson, '*.test.js': SiJest, '*.less': SiLess, '*.md': SiMarkdown, '*.mdx': SiMdx, 'mintlify.json': SiMintlify, 'mocha.opts': SiMocha, '*.mustache': SiHandlebarsdotjs, '*.sql': SiMysql, 'next.config.*': SiNextdotjs, '*.pl': SiPerl, '*.php': SiPhp, 'postcss.config.*': SiPostcss, 'prettier.config.*': SiPrettier, '*.prisma': SiPrisma, '*.pug': SiPug, '*.py': SiPython, '*.r': SiR, '*.rb': SiRuby, '*.jsx': SiReact, '*.tsx': SiReact, 'readme.md': SiReadme, '*.rdb': SiRedis, 'remix.config.*': SiRemix, '*.riv': SiRive, 'rollup.config.*': SiRollupdotjs, 'sanity.config.*': SiSanity, '*.sass': SiSass, '*.scss': SiSass, '*.sc': SiScala, '*.scala': SiScala, 'sentry.client.config.*': SiSentry, 'components.json': SiShadcnui, 'storybook.config.*': SiStorybook, 'stylelint.config.*': SiStylelint, '.sublime-settings': SiSublimetext, '*.svelte': SiSvelte, '*.svg': SiSvg, '*.swift': SiSwift, 'tailwind.config.*': SiTailwindcss, '*.toml': SiToml, '*.ts': SiTypescript, 'vercel.json': SiVercel, 'vite.config.*': SiVite, '*.vue': SiVuedotjs, '*.wasm': SiWebassembly,};const lineNumberClassNames = cn( '[&_code]:[counter-reset:line]', '[&_code]:[counter-increment:line_0]', '[&_.line]:before:content-[counter(line)]', '[&_.line]:before:inline-block', '[&_.line]:before:[counter-increment:line]', '[&_.line]:before:w-4', '[&_.line]:before:mr-4', '[&_.line]:before:text-[13px]', '[&_.line]:before:text-right', '[&_.line]:before:text-muted-foreground/50', '[&_.line]:before:font-mono', '[&_.line]:before:select-none');const darkModeClassNames = cn( 'dark:[&_.shiki]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]', 'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]', 'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]');const lineHighlightClassNames = cn( '[&_.line.highlighted]:bg-blue-50', '[&_.line.highlighted]:after:bg-blue-500', '[&_.line.highlighted]:after:absolute', '[&_.line.highlighted]:after:left-0', '[&_.line.highlighted]:after:top-0', '[&_.line.highlighted]:after:bottom-0', '[&_.line.highlighted]:after:w-0.5', 'dark:[&_.line.highlighted]:!bg-blue-500/10');const lineDiffClassNames = cn( '[&_.line.diff]:after:absolute', '[&_.line.diff]:after:left-0', '[&_.line.diff]:after:top-0', '[&_.line.diff]:after:bottom-0', '[&_.line.diff]:after:w-0.5', '[&_.line.diff.add]:bg-emerald-50', '[&_.line.diff.add]:after:bg-emerald-500', '[&_.line.diff.remove]:bg-rose-50', '[&_.line.diff.remove]:after:bg-rose-500', 'dark:[&_.line.diff.add]:!bg-emerald-500/10', 'dark:[&_.line.diff.remove]:!bg-rose-500/10');const lineFocusedClassNames = cn( '[&_code:has(.focused)_.line]:blur-[2px]', '[&_code:has(.focused)_.line.focused]:blur-none');const wordHighlightClassNames = cn( '[&_.highlighted-word]:bg-blue-50', 'dark:[&_.highlighted-word]:!bg-blue-500/10');const codeBlockClassName = cn( 'mt-0 bg-background text-sm', '[&_pre]:py-4', '[&_.shiki]:!bg-[var(--shiki-bg)]', '[&_code]:w-full', '[&_code]:grid', '[&_code]:overflow-x-auto', '[&_code]:bg-transparent', '[&_.line]:px-4', '[&_.line]:w-full', '[&_.line]:relative');type CodeBlockData = { language: string; filename: string; code: string;};type CodeBlockContextType = { value: string | undefined; onValueChange: ((value: string) => void) | undefined; data: CodeBlockData[];};const CodeBlockContext = createContext<CodeBlockContextType>({ value: undefined, onValueChange: undefined, data: [],});export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; data: CodeBlockData[];};export const CodeBlock = ({ value: controlledValue, onValueChange: controlledOnValueChange, defaultValue, className, data, ...props}: CodeBlockProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); return ( <CodeBlockContext.Provider value={{ value, onValueChange, data }}> <div className={cn('size-full overflow-hidden rounded-md border', className)} {...props} /> </CodeBlockContext.Provider> );};export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;export const CodeBlockHeader = ({ className, ...props}: CodeBlockHeaderProps) => ( <div className={cn( 'flex flex-row items-center border-b bg-secondary p-1', className )} {...props} />);export type CodeBlockFilesProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockFiles = ({ className, children, ...props}: CodeBlockFilesProps) => { const { data } = useContext(CodeBlockContext); return ( <div className={cn('flex grow flex-row items-center gap-2', className)} {...props} > {data.map(children)} </div> );};export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & { icon?: IconType; value?: string;};export const CodeBlockFilename = ({ className, icon, value, children, ...props}: CodeBlockFilenameProps) => { const { value: activeValue } = useContext(CodeBlockContext); const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => { const regex = new RegExp( `^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$` ); return regex.test(children as string); })?.[1]; const Icon = icon ?? defaultIcon; if (value !== activeValue) { return null; } return ( <div className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs" {...props} > {Icon && <Icon className="h-4 w-4 shrink-0" />} <span className="flex-1 truncate">{children}</span> </div> );};export type CodeBlockSelectProps = ComponentProps<typeof Select>;export const CodeBlockSelect = (props: CodeBlockSelectProps) => { const { value, onValueChange } = useContext(CodeBlockContext); return <Select onValueChange={onValueChange} value={value} {...props} />;};export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;export const CodeBlockSelectTrigger = ({ className, ...props}: CodeBlockSelectTriggerProps) => ( <SelectTrigger className={cn( 'w-fit border-none text-muted-foreground text-xs shadow-none', className )} {...props} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...props} />);export type CodeBlockSelectContentProps = Omit< ComponentProps<typeof SelectContent>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockSelectContent = ({ children, ...props}: CodeBlockSelectContentProps) => { const { data } = useContext(CodeBlockContext); return <SelectContent {...props}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...props} />);export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};export const CodeBlockCopyButton = ({ asChild, onCopy, onError, timeout = 2000, children, className, ...props}: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const { data, value } = useContext(CodeBlockContext); const code = data.find((item) => item.language === value)?.code; const copyToClipboard = () => { if ( typeof window === 'undefined' || !navigator.clipboard.writeText || !code ) { return; } navigator.clipboard.writeText(code).then(() => { setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); }, onError); }; if (asChild) { return cloneElement(children as ReactElement, { // @ts-expect-error - we know this is a button onClick: copyToClipboard, }); } const Icon = isCopied ? CheckIcon : CopyIcon; return ( <Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...props}> <pre className="w-full"> <code> {children ?.toString() .split('\n') .map((line, i) => ( <span className="line" key={i}> {line} </span> ))} </code> </pre> </div>);export type CodeBlockBodyProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => { const { data } = useContext(CodeBlockContext); return <div {...props}>{data.map(children)}</div>;};export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & { value: string; lineNumbers?: boolean;};export const CodeBlockItem = ({ children, lineNumbers = true, className, value, ...props}: CodeBlockItemProps) => { const { value: activeValue } = useContext(CodeBlockContext); if (value !== activeValue) { return null; } return ( <div className={cn( codeBlockClassName, lineHighlightClassNames, lineDiffClassNames, lineFocusedClassNames, wordHighlightClassNames, darkModeClassNames, lineNumbers && lineNumberClassNames, className )} {...props} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: any; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes, language, syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;};
Custom Theme
Loading component...
'use client';import type { BundledLanguage } from '@/components/ui/shadcn-io/code-block';import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockFilename, CodeBlockFiles, CodeBlockHeader, CodeBlockItem, CodeBlockSelect, CodeBlockSelectContent, CodeBlockSelectItem, CodeBlockSelectTrigger, CodeBlockSelectValue,} from '@/components/ui/shadcn-io/code-block';const code = [ { language: 'jsx', filename: 'MyComponent.jsx', code: `function MyComponent(props) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, }, { language: 'tsx', filename: 'MyComponent.tsx', code: `function MyComponent(props: { name: string }) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, },];const Example = () => ( <CodeBlock data={code} defaultValue={code[0].language}> <CodeBlockHeader> <CodeBlockFiles> {(item) => ( <CodeBlockFilename key={item.language} value={item.language}> {item.filename} </CodeBlockFilename> )} </CodeBlockFiles> <CodeBlockSelect> <CodeBlockSelectTrigger> <CodeBlockSelectValue /> </CodeBlockSelectTrigger> <CodeBlockSelectContent> {(item) => ( <CodeBlockSelectItem key={item.language} value={item.language}> {item.language} </CodeBlockSelectItem> )} </CodeBlockSelectContent> </CodeBlockSelect> <CodeBlockCopyButton onCopy={() => console.log('Copied code to clipboard')} onError={() => console.error('Failed to copy code to clipboard')} /> </CodeBlockHeader> <CodeBlockBody> {(item) => ( <CodeBlockItem key={item.language} value={item.language}> <CodeBlockContent language={item.language as BundledLanguage} themes={{ light: 'vitesse-light', dark: 'vitesse-dark', }} > {item.code} </CodeBlockContent> </CodeBlockItem> )} </CodeBlockBody> </CodeBlock>);export default Example;
'use client';import { type IconType, SiAstro, SiBiome, SiBower, SiBun, SiC, SiCircleci, SiCoffeescript, SiCplusplus, SiCss, SiCssmodules, SiDart, SiDocker, SiDocusaurus, SiDotenv, SiEditorconfig, SiEslint, SiGatsby, SiGitignoredotio, SiGnubash, SiGo, SiGraphql, SiGrunt, SiGulp, SiHandlebarsdotjs, SiHtml5, SiJavascript, SiJest, SiJson, SiLess, SiMarkdown, SiMdx, SiMintlify, SiMocha, SiMysql, SiNextdotjs, SiPerl, SiPhp, SiPostcss, SiPrettier, SiPrisma, SiPug, SiPython, SiR, SiReact, SiReadme, SiRedis, SiRemix, SiRive, SiRollupdotjs, SiRuby, SiSanity, SiSass, SiScala, SiSentry, SiShadcnui, SiStorybook, SiStylelint, SiSublimetext, SiSvelte, SiSvg, SiSwift, SiTailwindcss, SiToml, SiTypescript, SiVercel, SiVite, SiVuedotjs, SiWebassembly,} from '@icons-pack/react-simple-icons';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { CheckIcon, CopyIcon } from 'lucide-react';import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode,} from 'react';import { cloneElement, createContext, useContext, useEffect, useState,} from 'react';import { Button } from '@/components/ui/button';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';import { cn } from '@/lib/utils';export type BundledLanguage = string;const filenameIconMap = { '.env': SiDotenv, '*.astro': SiAstro, 'biome.json': SiBiome, '.bowerrc': SiBower, 'bun.lockb': SiBun, '*.c': SiC, '*.cpp': SiCplusplus, '.circleci/config.yml': SiCircleci, '*.coffee': SiCoffeescript, '*.module.css': SiCssmodules, '*.css': SiCss, '*.dart': SiDart, Dockerfile: SiDocker, 'docusaurus.config.js': SiDocusaurus, '.editorconfig': SiEditorconfig, '.eslintrc': SiEslint, 'eslint.config.*': SiEslint, 'gatsby-config.*': SiGatsby, '.gitignore': SiGitignoredotio, '*.go': SiGo, '*.graphql': SiGraphql, '*.sh': SiGnubash, 'Gruntfile.*': SiGrunt, 'gulpfile.*': SiGulp, '*.hbs': SiHandlebarsdotjs, '*.html': SiHtml5, '*.js': SiJavascript, '*.json': SiJson, '*.test.js': SiJest, '*.less': SiLess, '*.md': SiMarkdown, '*.mdx': SiMdx, 'mintlify.json': SiMintlify, 'mocha.opts': SiMocha, '*.mustache': SiHandlebarsdotjs, '*.sql': SiMysql, 'next.config.*': SiNextdotjs, '*.pl': SiPerl, '*.php': SiPhp, 'postcss.config.*': SiPostcss, 'prettier.config.*': SiPrettier, '*.prisma': SiPrisma, '*.pug': SiPug, '*.py': SiPython, '*.r': SiR, '*.rb': SiRuby, '*.jsx': SiReact, '*.tsx': SiReact, 'readme.md': SiReadme, '*.rdb': SiRedis, 'remix.config.*': SiRemix, '*.riv': SiRive, 'rollup.config.*': SiRollupdotjs, 'sanity.config.*': SiSanity, '*.sass': SiSass, '*.scss': SiSass, '*.sc': SiScala, '*.scala': SiScala, 'sentry.client.config.*': SiSentry, 'components.json': SiShadcnui, 'storybook.config.*': SiStorybook, 'stylelint.config.*': SiStylelint, '.sublime-settings': SiSublimetext, '*.svelte': SiSvelte, '*.svg': SiSvg, '*.swift': SiSwift, 'tailwind.config.*': SiTailwindcss, '*.toml': SiToml, '*.ts': SiTypescript, 'vercel.json': SiVercel, 'vite.config.*': SiVite, '*.vue': SiVuedotjs, '*.wasm': SiWebassembly,};const lineNumberClassNames = cn( '[&_code]:[counter-reset:line]', '[&_code]:[counter-increment:line_0]', '[&_.line]:before:content-[counter(line)]', '[&_.line]:before:inline-block', '[&_.line]:before:[counter-increment:line]', '[&_.line]:before:w-4', '[&_.line]:before:mr-4', '[&_.line]:before:text-[13px]', '[&_.line]:before:text-right', '[&_.line]:before:text-muted-foreground/50', '[&_.line]:before:font-mono', '[&_.line]:before:select-none');const darkModeClassNames = cn( 'dark:[&_.shiki]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]', 'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]', 'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]', 'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]', 'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]', 'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]');const lineHighlightClassNames = cn( '[&_.line.highlighted]:bg-blue-50', '[&_.line.highlighted]:after:bg-blue-500', '[&_.line.highlighted]:after:absolute', '[&_.line.highlighted]:after:left-0', '[&_.line.highlighted]:after:top-0', '[&_.line.highlighted]:after:bottom-0', '[&_.line.highlighted]:after:w-0.5', 'dark:[&_.line.highlighted]:!bg-blue-500/10');const lineDiffClassNames = cn( '[&_.line.diff]:after:absolute', '[&_.line.diff]:after:left-0', '[&_.line.diff]:after:top-0', '[&_.line.diff]:after:bottom-0', '[&_.line.diff]:after:w-0.5', '[&_.line.diff.add]:bg-emerald-50', '[&_.line.diff.add]:after:bg-emerald-500', '[&_.line.diff.remove]:bg-rose-50', '[&_.line.diff.remove]:after:bg-rose-500', 'dark:[&_.line.diff.add]:!bg-emerald-500/10', 'dark:[&_.line.diff.remove]:!bg-rose-500/10');const lineFocusedClassNames = cn( '[&_code:has(.focused)_.line]:blur-[2px]', '[&_code:has(.focused)_.line.focused]:blur-none');const wordHighlightClassNames = cn( '[&_.highlighted-word]:bg-blue-50', 'dark:[&_.highlighted-word]:!bg-blue-500/10');const codeBlockClassName = cn( 'mt-0 bg-background text-sm', '[&_pre]:py-4', '[&_.shiki]:!bg-[var(--shiki-bg)]', '[&_code]:w-full', '[&_code]:grid', '[&_code]:overflow-x-auto', '[&_code]:bg-transparent', '[&_.line]:px-4', '[&_.line]:w-full', '[&_.line]:relative');type CodeBlockData = { language: string; filename: string; code: string;};type CodeBlockContextType = { value: string | undefined; onValueChange: ((value: string) => void) | undefined; data: CodeBlockData[];};const CodeBlockContext = createContext<CodeBlockContextType>({ value: undefined, onValueChange: undefined, data: [],});export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; data: CodeBlockData[];};export const CodeBlock = ({ value: controlledValue, onValueChange: controlledOnValueChange, defaultValue, className, data, ...props}: CodeBlockProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); return ( <CodeBlockContext.Provider value={{ value, onValueChange, data }}> <div className={cn('size-full overflow-hidden rounded-md border', className)} {...props} /> </CodeBlockContext.Provider> );};export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;export const CodeBlockHeader = ({ className, ...props}: CodeBlockHeaderProps) => ( <div className={cn( 'flex flex-row items-center border-b bg-secondary p-1', className )} {...props} />);export type CodeBlockFilesProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockFiles = ({ className, children, ...props}: CodeBlockFilesProps) => { const { data } = useContext(CodeBlockContext); return ( <div className={cn('flex grow flex-row items-center gap-2', className)} {...props} > {data.map(children)} </div> );};export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & { icon?: IconType; value?: string;};export const CodeBlockFilename = ({ className, icon, value, children, ...props}: CodeBlockFilenameProps) => { const { value: activeValue } = useContext(CodeBlockContext); const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => { const regex = new RegExp( `^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$` ); return regex.test(children as string); })?.[1]; const Icon = icon ?? defaultIcon; if (value !== activeValue) { return null; } return ( <div className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs" {...props} > {Icon && <Icon className="h-4 w-4 shrink-0" />} <span className="flex-1 truncate">{children}</span> </div> );};export type CodeBlockSelectProps = ComponentProps<typeof Select>;export const CodeBlockSelect = (props: CodeBlockSelectProps) => { const { value, onValueChange } = useContext(CodeBlockContext); return <Select onValueChange={onValueChange} value={value} {...props} />;};export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;export const CodeBlockSelectTrigger = ({ className, ...props}: CodeBlockSelectTriggerProps) => ( <SelectTrigger className={cn( 'w-fit border-none text-muted-foreground text-xs shadow-none', className )} {...props} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...props} />);export type CodeBlockSelectContentProps = Omit< ComponentProps<typeof SelectContent>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockSelectContent = ({ children, ...props}: CodeBlockSelectContentProps) => { const { data } = useContext(CodeBlockContext); return <SelectContent {...props}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...props} />);export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};export const CodeBlockCopyButton = ({ asChild, onCopy, onError, timeout = 2000, children, className, ...props}: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const { data, value } = useContext(CodeBlockContext); const code = data.find((item) => item.language === value)?.code; const copyToClipboard = () => { if ( typeof window === 'undefined' || !navigator.clipboard.writeText || !code ) { return; } navigator.clipboard.writeText(code).then(() => { setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); }, onError); }; if (asChild) { return cloneElement(children as ReactElement, { // @ts-expect-error - we know this is a button onClick: copyToClipboard, }); } const Icon = isCopied ? CheckIcon : CopyIcon; return ( <Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...props}> <pre className="w-full"> <code> {children ?.toString() .split('\n') .map((line, i) => ( <span className="line" key={i}> {line} </span> ))} </code> </pre> </div>);export type CodeBlockBodyProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (item: CodeBlockData) => ReactNode;};export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => { const { data } = useContext(CodeBlockContext); return <div {...props}>{data.map(children)}</div>;};export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & { value: string; lineNumbers?: boolean;};export const CodeBlockItem = ({ children, lineNumbers = true, className, value, ...props}: CodeBlockItemProps) => { const { value: activeValue } = useContext(CodeBlockContext); if (value !== activeValue) { return null; } return ( <div className={cn( codeBlockClassName, lineHighlightClassNames, lineDiffClassNames, lineFocusedClassNames, wordHighlightClassNames, darkModeClassNames, lineNumbers && lineNumberClassNames, className )} {...props} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: any; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes, language, syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;};
Use Cases
This free open source React component works well for:
- Documentation sites - Code examples and tutorials for developer resources built with Next.js
- Technical blogs - Syntax-highlighted code snippets in articles using TypeScript
- API documentation - Request/response examples and code samples
- Educational platforms - Programming tutorials and interactive coding lessons using shadcn/ui design
- Code portfolios - Showcasing projects and technical implementations
- Developer tools - Code viewers and syntax validators using Tailwind CSS styling
API Reference
CodeBlock
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | undefined | CodeBlockHeader, CodeBlockBody, and content |
className | string | undefined | Additional CSS classes for the container |
CodeBlockHeader
Prop | Type | Default | Description |
---|---|---|---|
filename | string | undefined | File name to display in the header |
language | string | undefined | Programming language for icon display |
className | string | undefined | Additional CSS classes for the header |
CodeBlockBody
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | undefined | CodeBlockContent or other body elements |
className | string | undefined | Additional CSS classes for the body |
CodeBlockContent
Prop | Type | Default | Description |
---|---|---|---|
code | string | undefined | Code string to highlight and display |
language | string | "text" | Programming language for syntax highlighting |
highlightLines | number[] | [] | Array of line numbers to highlight |
highlightWords | string[] | [] | Array of words/patterns to highlight |
focusLines | number[] | [] | Array of line numbers to focus/emphasize |
showLineNumbers | boolean | true | Whether to display line numbers |
showCopyButton | boolean | true | Whether to show the copy to clipboard button |
theme | string | "github-dark" | Shiki theme for syntax highlighting |
className | string | undefined | Additional CSS classes for the content |
Implementation Notes
- Component uses Shiki for syntax highlighting with support for 100+ programming languages
- Server-side rendering supported through separate
code-block/server
import for RSC compatibility - Copy functionality uses native Clipboard API with fallback for older browsers
- Line highlighting supports both individual numbers and ranges (e.g., [1, 3, 5-8])
- Word highlighting uses regex patterns for flexible text matching and emphasis
- Language detection automatic based on file extensions or manual override available
- Theme system supports both built-in Shiki themes and custom color schemes
- Compatible with shadcn/ui design system and Tailwind CSS utility classes
- Performance optimized with code splitting between client and server components
- Diff visualization uses standard Git diff format with + and - line prefixes
Ripple Button
Interactive button with animated ripple effects and multiple variants. Perfect for React applications requiring engaging button interactions with Next.js integration and TypeScript support.
Code Editor
Animated code editor component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring syntax highlighting, typing animations, and customizable themes.