Code (5)
Shadcn.io is not affiliated with official shadcn/ui
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;Sign in to view the code
'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 as any)} /> </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 as any)} />);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 as any)} > {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 as any)} > {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 as any)} />;};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 as any)} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...(props as any)} />);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 as any)}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...(props as any)} />);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 as any)} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...(props as any)}> <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 as any)}>{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 as any)} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: { light: string; dark: string; }; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes = { light: 'vitesse-light', dark: 'vitesse-dark', }, language = 'typescript', syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { const [highlightedCode, setHighlightedCode] = useState<string>(''); const [isLoading, setIsLoading] = useState(syntaxHighlighting); useEffect(() => { if (!syntaxHighlighting) { setIsLoading(false); return; } const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const html = await codeToHtml(children, { lang: language, themes: { light: themes.light, dark: themes.dark, }, }); setHighlightedCode(html); setIsLoading(false); } catch (error) { console.error(`Failed to highlight code for language "${language}":`, error); setIsLoading(false); } }; loadHighlightedCode(); }, [children, language, themes, syntaxHighlighting]); if (!syntaxHighlighting || isLoading) { return <CodeBlockFallback {...(props as any)}>{children}</CodeBlockFallback>; } return ( <div dangerouslySetInnerHTML={{ __html: highlightedCode }} {...(props as any)} /> );};Sign in to view the source
Installation
npx shadcn@latest add https://www.shadcn.io/registry/code-block.jsonnpx shadcn@latest add https://www.shadcn.io/registry/code-block.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/code-block.jsonbunx shadcn@latest add https://www.shadcn.io/registry/code-block.jsonSign in to access installation commands
Sign inFeatures
- 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;Sign in to view the code
'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 as any)} /> </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 as any)} />);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 as any)} > {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 as any)} > {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 as any)} />;};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 as any)} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...(props as any)} />);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 as any)}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...(props as any)} />);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 as any)} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...(props as any)}> <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 as any)}>{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 as any)} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: { light: string; dark: string; }; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes = { light: 'vitesse-light', dark: 'vitesse-dark', }, language = 'typescript', syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { const [highlightedCode, setHighlightedCode] = useState<string>(''); const [isLoading, setIsLoading] = useState(syntaxHighlighting); useEffect(() => { if (!syntaxHighlighting) { setIsLoading(false); return; } const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const html = await codeToHtml(children, { lang: language, themes: { light: themes.light, dark: themes.dark, }, }); setHighlightedCode(html); setIsLoading(false); } catch (error) { console.error(`Failed to highlight code for language "${language}":`, error); setIsLoading(false); } }; loadHighlightedCode(); }, [children, language, themes, syntaxHighlighting]); if (!syntaxHighlighting || isLoading) { return <CodeBlockFallback {...(props as any)}>{children}</CodeBlockFallback>; } return ( <div dangerouslySetInnerHTML={{ __html: highlightedCode }} {...(props as any)} /> );};Sign in to view the source
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;Sign in to view the code
'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 as any)} /> </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 as any)} />);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 as any)} > {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 as any)} > {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 as any)} />;};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 as any)} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...(props as any)} />);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 as any)}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...(props as any)} />);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 as any)} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...(props as any)}> <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 as any)}>{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 as any)} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: { light: string; dark: string; }; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes = { light: 'vitesse-light', dark: 'vitesse-dark', }, language = 'typescript', syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { const [highlightedCode, setHighlightedCode] = useState<string>(''); const [isLoading, setIsLoading] = useState(syntaxHighlighting); useEffect(() => { if (!syntaxHighlighting) { setIsLoading(false); return; } const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const html = await codeToHtml(children, { lang: language, themes: { light: themes.light, dark: themes.dark, }, }); setHighlightedCode(html); setIsLoading(false); } catch (error) { console.error(`Failed to highlight code for language "${language}":`, error); setIsLoading(false); } }; loadHighlightedCode(); }, [children, language, themes, syntaxHighlighting]); if (!syntaxHighlighting || isLoading) { return <CodeBlockFallback {...(props as any)}>{children}</CodeBlockFallback>; } return ( <div dangerouslySetInnerHTML={{ __html: highlightedCode }} {...(props as any)} /> );};Sign in to view the source
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;Sign in to view the code
'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 as any)} /> </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 as any)} />);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 as any)} > {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 as any)} > {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 as any)} />;};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 as any)} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...(props as any)} />);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 as any)}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...(props as any)} />);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 as any)} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...(props as any)}> <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 as any)}>{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 as any)} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: { light: string; dark: string; }; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes = { light: 'vitesse-light', dark: 'vitesse-dark', }, language = 'typescript', syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { const [highlightedCode, setHighlightedCode] = useState<string>(''); const [isLoading, setIsLoading] = useState(syntaxHighlighting); useEffect(() => { if (!syntaxHighlighting) { setIsLoading(false); return; } const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const html = await codeToHtml(children, { lang: language, themes: { light: themes.light, dark: themes.dark, }, }); setHighlightedCode(html); setIsLoading(false); } catch (error) { console.error(`Failed to highlight code for language "${language}":`, error); setIsLoading(false); } }; loadHighlightedCode(); }, [children, language, themes, syntaxHighlighting]); if (!syntaxHighlighting || isLoading) { return <CodeBlockFallback {...(props as any)}>{children}</CodeBlockFallback>; } return ( <div dangerouslySetInnerHTML={{ __html: highlightedCode }} {...(props as any)} /> );};Sign in to view the source
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;Sign in to view the code
'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 as any)} /> </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 as any)} />);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 as any)} > {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 as any)} > {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 as any)} />;};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 as any)} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...(props as any)} />);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 as any)}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...(props as any)} />);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 as any)} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...(props as any)}> <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 as any)}>{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 as any)} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: { light: string; dark: string; }; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes = { light: 'vitesse-light', dark: 'vitesse-dark', }, language = 'typescript', syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { const [highlightedCode, setHighlightedCode] = useState<string>(''); const [isLoading, setIsLoading] = useState(syntaxHighlighting); useEffect(() => { if (!syntaxHighlighting) { setIsLoading(false); return; } const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const html = await codeToHtml(children, { lang: language, themes: { light: themes.light, dark: themes.dark, }, }); setHighlightedCode(html); setIsLoading(false); } catch (error) { console.error(`Failed to highlight code for language "${language}":`, error); setIsLoading(false); } }; loadHighlightedCode(); }, [children, language, themes, syntaxHighlighting]); if (!syntaxHighlighting || isLoading) { return <CodeBlockFallback {...(props as any)}>{children}</CodeBlockFallback>; } return ( <div dangerouslySetInnerHTML={{ __html: highlightedCode }} {...(props as any)} /> );};Sign in to view the source
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;Sign in to view the code
'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 as any)} /> </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 as any)} />);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 as any)} > {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 as any)} > {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 as any)} />;};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 as any)} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...(props as any)} />);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 as any)}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...(props as any)} />);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 as any)} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...(props as any)}> <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 as any)}>{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 as any)} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: { light: string; dark: string; }; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes = { light: 'vitesse-light', dark: 'vitesse-dark', }, language = 'typescript', syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { const [highlightedCode, setHighlightedCode] = useState<string>(''); const [isLoading, setIsLoading] = useState(syntaxHighlighting); useEffect(() => { if (!syntaxHighlighting) { setIsLoading(false); return; } const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const html = await codeToHtml(children, { lang: language, themes: { light: themes.light, dark: themes.dark, }, }); setHighlightedCode(html); setIsLoading(false); } catch (error) { console.error(`Failed to highlight code for language "${language}":`, error); setIsLoading(false); } }; loadHighlightedCode(); }, [children, language, themes, syntaxHighlighting]); if (!syntaxHighlighting || isLoading) { return <CodeBlockFallback {...(props as any)}>{children}</CodeBlockFallback>; } return ( <div dangerouslySetInnerHTML={{ __html: highlightedCode }} {...(props as any)} /> );};Sign in to view the source
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;Sign in to view the code
'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 as any)} /> </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 as any)} />);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 as any)} > {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 as any)} > {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 as any)} />;};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 as any)} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...(props as any)} />);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 as any)}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...(props as any)} />);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 as any)} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...(props as any)}> <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 as any)}>{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 as any)} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: { light: string; dark: string; }; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes = { light: 'vitesse-light', dark: 'vitesse-dark', }, language = 'typescript', syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { const [highlightedCode, setHighlightedCode] = useState<string>(''); const [isLoading, setIsLoading] = useState(syntaxHighlighting); useEffect(() => { if (!syntaxHighlighting) { setIsLoading(false); return; } const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const html = await codeToHtml(children, { lang: language, themes: { light: themes.light, dark: themes.dark, }, }); setHighlightedCode(html); setIsLoading(false); } catch (error) { console.error(`Failed to highlight code for language "${language}":`, error); setIsLoading(false); } }; loadHighlightedCode(); }, [children, language, themes, syntaxHighlighting]); if (!syntaxHighlighting || isLoading) { return <CodeBlockFallback {...(props as any)}>{children}</CodeBlockFallback>; } return ( <div dangerouslySetInnerHTML={{ __html: highlightedCode }} {...(props as any)} /> );};Sign in to view the source
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;Sign in to view the code
'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 as any)} /> </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 as any)} />);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 as any)} > {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 as any)} > {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 as any)} />;};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 as any)} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...(props as any)} />);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 as any)}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...(props as any)} />);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 as any)} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...(props as any)}> <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 as any)}>{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 as any)} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: { light: string; dark: string; }; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes = { light: 'vitesse-light', dark: 'vitesse-dark', }, language = 'typescript', syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { const [highlightedCode, setHighlightedCode] = useState<string>(''); const [isLoading, setIsLoading] = useState(syntaxHighlighting); useEffect(() => { if (!syntaxHighlighting) { setIsLoading(false); return; } const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const html = await codeToHtml(children, { lang: language, themes: { light: themes.light, dark: themes.dark, }, }); setHighlightedCode(html); setIsLoading(false); } catch (error) { console.error(`Failed to highlight code for language "${language}":`, error); setIsLoading(false); } }; loadHighlightedCode(); }, [children, language, themes, syntaxHighlighting]); if (!syntaxHighlighting || isLoading) { return <CodeBlockFallback {...(props as any)}>{children}</CodeBlockFallback>; } return ( <div dangerouslySetInnerHTML={{ __html: highlightedCode }} {...(props as any)} /> );};Sign in to view the source
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;Sign in to view the code
'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 as any)} /> </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 as any)} />);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 as any)} > {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 as any)} > {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 as any)} />;};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 as any)} />);export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => ( <SelectValue {...(props as any)} />);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 as any)}>{data.map(children)}</SelectContent>;};export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;export const CodeBlockSelectItem = ({ className, ...props}: CodeBlockSelectItemProps) => ( <SelectItem className={cn('text-sm', className)} {...(props as any)} />);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 as any)} > {children ?? <Icon className="text-muted-foreground" size={14} />} </Button> );};type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => ( <div {...(props as any)}> <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 as any)}>{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 as any)} > {children} </div> );};export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & { themes?: { light: string; dark: string; }; language?: BundledLanguage; syntaxHighlighting?: boolean; children: string;};export const CodeBlockContent = ({ children, themes = { light: 'vitesse-light', dark: 'vitesse-dark', }, language = 'typescript', syntaxHighlighting = true, ...props}: CodeBlockContentProps) => { const [highlightedCode, setHighlightedCode] = useState<string>(''); const [isLoading, setIsLoading] = useState(syntaxHighlighting); useEffect(() => { if (!syntaxHighlighting) { setIsLoading(false); return; } const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const html = await codeToHtml(children, { lang: language, themes: { light: themes.light, dark: themes.dark, }, }); setHighlightedCode(html); setIsLoading(false); } catch (error) { console.error(`Failed to highlight code for language "${language}":`, error); setIsLoading(false); } }; loadHighlightedCode(); }, [children, language, themes, syntaxHighlighting]); if (!syntaxHighlighting || isLoading) { return <CodeBlockFallback {...(props as any)}>{children}</CodeBlockFallback>; } return ( <div dangerouslySetInnerHTML={{ __html: highlightedCode }} {...(props as any)} /> );};Sign in to view the source
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/serverimport 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