Code Editor
Animated code editor component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring syntax highlighting, typing animations, and customizable themes.
Powered by
import { CodeEditor } from '@/components/ui/shadcn-io/code-editor';import { Code } from 'lucide-react';export const CodeEditorDemo = () => { return ( <CodeEditor cursor className="w-[640px] h-[480px]" lang="tsx" title="component.tsx" icon={<Code />} duration={15} delay={0.5} copyButton > {`'use client';import * as React from 'react';type MyComponentProps = { myProps: string;} & React.HTMLAttributes<HTMLDivElement>;const MyComponent = React.forwardRef<HTMLDivElement, MyComponentProps>( ({ myProps, ...props }, ref) => { return ( <div ref={ref} {...props}> <p>My Component</p> </div> ); },);MyComponent.displayName = 'MyComponent';export { MyComponent, type MyComponentProps };`} </CodeEditor> );};export default CodeEditorDemo;
'use client';import * as React from 'react';import { useInView, type UseInViewOptions } from 'motion/react';import { useTheme } from 'next-themes';import { cn } from '@/lib/utils';import { Button } from '@/components/ui/button';import { Copy, Check } from 'lucide-react';type CopyButtonProps = { content: string; size?: 'sm' | 'default' | 'lg'; variant?: 'default' | 'ghost' | 'outline'; className?: string; onCopy?: (content: string) => void;};function CopyButton({ content, size = 'default', variant = 'default', className, onCopy,}: CopyButtonProps) { const [copied, setCopied] = React.useState(false); const handleCopy = async () => { try { await navigator.clipboard.writeText(content); setCopied(true); onCopy?.(content); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy text: ', err); } }; return ( <Button size={size} variant={variant} onClick={handleCopy} className={cn('h-8 w-8 p-0', className)} > {copied ? ( <Check className="h-3 w-3" /> ) : ( <Copy className="h-3 w-3" /> )} </Button> );}type CodeEditorProps = Omit<React.ComponentProps<'div'>, 'onCopy'> & { children: string; lang: string; themes?: { light: string; dark: string; }; duration?: number; delay?: number; header?: boolean; dots?: boolean; icon?: React.ReactNode; cursor?: boolean; inView?: boolean; inViewMargin?: UseInViewOptions['margin']; inViewOnce?: boolean; copyButton?: boolean; writing?: boolean; title?: string; onDone?: () => void; onCopy?: (content: string) => void;};function CodeEditor({ children: code, lang, themes = { light: 'github-light', dark: 'github-dark', }, duration = 5, delay = 0, className, header = true, dots = true, icon, cursor = false, inView = false, inViewMargin = '0px', inViewOnce = true, copyButton = false, writing = true, title, onDone, onCopy, ...props}: CodeEditorProps) { const { resolvedTheme } = useTheme(); const editorRef = React.useRef<HTMLDivElement>(null); const [visibleCode, setVisibleCode] = React.useState(''); const [highlightedCode, setHighlightedCode] = React.useState(''); const [isDone, setIsDone] = React.useState(false); const inViewResult = useInView(editorRef, { once: inViewOnce, margin: inViewMargin, }); const isInView = !inView || inViewResult; React.useEffect(() => { if (!visibleCode.length || !isInView) return; const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const highlighted = await codeToHtml(visibleCode, { lang, themes: { light: themes.light, dark: themes.dark, }, defaultColor: resolvedTheme === 'dark' ? 'dark' : 'light', }); setHighlightedCode(highlighted); } catch (e) { console.error(`Language "${lang}" could not be loaded.`, e); } }; loadHighlightedCode(); }, [ lang, themes, writing, isInView, duration, delay, visibleCode, resolvedTheme, ]); React.useEffect(() => { if (!writing) { setVisibleCode(code); onDone?.(); return; } if (!code.length || !isInView) return; const characters = Array.from(code); let index = 0; const totalDuration = duration * 1000; const interval = totalDuration / characters.length; let intervalId: NodeJS.Timeout; const timeout = setTimeout(() => { intervalId = setInterval(() => { if (index < characters.length) { setVisibleCode((prev) => { const currentIndex = index; index += 1; return prev + characters[currentIndex]; }); editorRef.current?.scrollTo({ top: editorRef.current?.scrollHeight, behavior: 'smooth', }); } else { clearInterval(intervalId); setIsDone(true); onDone?.(); } }, interval); }, delay * 1000); return () => { clearTimeout(timeout); clearInterval(intervalId); }; }, [code, duration, delay, isInView, writing, onDone]); return ( <div data-slot="code-editor" className={cn( 'relative bg-muted/50 w-[600px] h-[400px] border border-border overflow-hidden flex flex-col rounded-xl', className, )} {...props} > {header ? ( <div className="bg-muted border-b border-border/75 dark:border-border/50 relative flex flex-row items-center justify-between gap-y-2 h-10 px-4"> {dots && ( <div className="flex flex-row gap-x-2"> <div className="size-2 rounded-full bg-red-500"></div> <div className="size-2 rounded-full bg-yellow-500"></div> <div className="size-2 rounded-full bg-green-500"></div> </div> )} {title && ( <div className={cn( 'flex flex-row items-center gap-2', dots && 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2', )} > {icon ? ( <div className="text-muted-foreground [&_svg]:size-3.5" dangerouslySetInnerHTML={ typeof icon === 'string' ? { __html: icon } : undefined } > {typeof icon !== 'string' ? icon : null} </div> ) : null} <figcaption className="flex-1 truncate text-muted-foreground text-[13px]"> {title} </figcaption> </div> )} {copyButton ? ( <CopyButton content={code} size="sm" variant="ghost" className="-me-2 bg-transparent hover:bg-black/5 dark:hover:bg-white/10" onCopy={onCopy} /> ) : null} </div> ) : ( copyButton && ( <CopyButton content={code} size="sm" variant="ghost" className="absolute right-2 top-2 z-[2] backdrop-blur-md bg-transparent hover:bg-black/5 dark:hover:bg-white/10" onCopy={onCopy} /> ) )} <div ref={editorRef} className="h-[calc(100%-2.75rem)] w-full text-sm p-4 font-mono relative overflow-auto flex-1" > <div className={cn( '[&>pre,_&_code]:!bg-transparent [&>pre,_&_code]:[background:transparent_!important] [&>pre,_&_code]:border-none [&_code]:!text-[13px]', cursor && !isDone && "[&_.line:last-of-type::after]:content-['|'] [&_.line:last-of-type::after]:animate-pulse [&_.line:last-of-type::after]:inline-block [&_.line:last-of-type::after]:w-[1ch] [&_.line:last-of-type::after]:-translate-px", )} dangerouslySetInnerHTML={{ __html: highlightedCode }} /> </div> </div> );}export { CodeEditor, CopyButton, type CodeEditorProps, type CopyButtonProps };
Installation
npx shadcn@latest add https://www.shadcn.io/registry/code-editor.json
npx shadcn@latest add https://www.shadcn.io/registry/code-editor.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/code-editor.json
bunx shadcn@latest add https://www.shadcn.io/registry/code-editor.json
Features
- Syntax highlighting - Multi-language support with Shiki integration using JavaScript parsing
- Typing animations - Realistic code writing simulation with configurable speed using Motion library
- Theme switching - Light/dark mode support with next-themes integration for React applications
- Copy functionality - Built-in copy button with custom callback support using TypeScript handlers
- Viewport detection - InView animations with configurable margins for Next.js projects
- Customizable header - macOS-style window with dots, title, and icon support using Tailwind CSS
- Cursor animation - Blinking cursor effect during typing simulation using CSS animations
- Open source - Free code editor component with shadcn/ui theming and accessibility features
Examples
With Custom Theme
import { CodeEditor } from '@/components/ui/shadcn-io/code-editor';import { FileCode } from 'lucide-react';export const CodeEditorThemeDemo = () => { return ( <CodeEditor cursor className="w-[640px] h-[480px]" lang="typescript" title="utils.ts" icon={<FileCode />} duration={10} delay={1} copyButton themes={{ light: 'vitesse-light', dark: 'vitesse-dark', }} > {`import { clsx, type ClassValue } from 'clsx';import { twMerge } from 'tailwind-merge';export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs));}export function formatDate(date: Date): string { return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric', }).format(date);}`} </CodeEditor> );};export default CodeEditorThemeDemo;
'use client';import * as React from 'react';import { useInView, type UseInViewOptions } from 'motion/react';import { useTheme } from 'next-themes';import { cn } from '@/lib/utils';import { Button } from '@/components/ui/button';import { Copy, Check } from 'lucide-react';type CopyButtonProps = { content: string; size?: 'sm' | 'default' | 'lg'; variant?: 'default' | 'ghost' | 'outline'; className?: string; onCopy?: (content: string) => void;};function CopyButton({ content, size = 'default', variant = 'default', className, onCopy,}: CopyButtonProps) { const [copied, setCopied] = React.useState(false); const handleCopy = async () => { try { await navigator.clipboard.writeText(content); setCopied(true); onCopy?.(content); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy text: ', err); } }; return ( <Button size={size} variant={variant} onClick={handleCopy} className={cn('h-8 w-8 p-0', className)} > {copied ? ( <Check className="h-3 w-3" /> ) : ( <Copy className="h-3 w-3" /> )} </Button> );}type CodeEditorProps = Omit<React.ComponentProps<'div'>, 'onCopy'> & { children: string; lang: string; themes?: { light: string; dark: string; }; duration?: number; delay?: number; header?: boolean; dots?: boolean; icon?: React.ReactNode; cursor?: boolean; inView?: boolean; inViewMargin?: UseInViewOptions['margin']; inViewOnce?: boolean; copyButton?: boolean; writing?: boolean; title?: string; onDone?: () => void; onCopy?: (content: string) => void;};function CodeEditor({ children: code, lang, themes = { light: 'github-light', dark: 'github-dark', }, duration = 5, delay = 0, className, header = true, dots = true, icon, cursor = false, inView = false, inViewMargin = '0px', inViewOnce = true, copyButton = false, writing = true, title, onDone, onCopy, ...props}: CodeEditorProps) { const { resolvedTheme } = useTheme(); const editorRef = React.useRef<HTMLDivElement>(null); const [visibleCode, setVisibleCode] = React.useState(''); const [highlightedCode, setHighlightedCode] = React.useState(''); const [isDone, setIsDone] = React.useState(false); const inViewResult = useInView(editorRef, { once: inViewOnce, margin: inViewMargin, }); const isInView = !inView || inViewResult; React.useEffect(() => { if (!visibleCode.length || !isInView) return; const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const highlighted = await codeToHtml(visibleCode, { lang, themes: { light: themes.light, dark: themes.dark, }, defaultColor: resolvedTheme === 'dark' ? 'dark' : 'light', }); setHighlightedCode(highlighted); } catch (e) { console.error(`Language "${lang}" could not be loaded.`, e); } }; loadHighlightedCode(); }, [ lang, themes, writing, isInView, duration, delay, visibleCode, resolvedTheme, ]); React.useEffect(() => { if (!writing) { setVisibleCode(code); onDone?.(); return; } if (!code.length || !isInView) return; const characters = Array.from(code); let index = 0; const totalDuration = duration * 1000; const interval = totalDuration / characters.length; let intervalId: NodeJS.Timeout; const timeout = setTimeout(() => { intervalId = setInterval(() => { if (index < characters.length) { setVisibleCode((prev) => { const currentIndex = index; index += 1; return prev + characters[currentIndex]; }); editorRef.current?.scrollTo({ top: editorRef.current?.scrollHeight, behavior: 'smooth', }); } else { clearInterval(intervalId); setIsDone(true); onDone?.(); } }, interval); }, delay * 1000); return () => { clearTimeout(timeout); clearInterval(intervalId); }; }, [code, duration, delay, isInView, writing, onDone]); return ( <div data-slot="code-editor" className={cn( 'relative bg-muted/50 w-[600px] h-[400px] border border-border overflow-hidden flex flex-col rounded-xl', className, )} {...props} > {header ? ( <div className="bg-muted border-b border-border/75 dark:border-border/50 relative flex flex-row items-center justify-between gap-y-2 h-10 px-4"> {dots && ( <div className="flex flex-row gap-x-2"> <div className="size-2 rounded-full bg-red-500"></div> <div className="size-2 rounded-full bg-yellow-500"></div> <div className="size-2 rounded-full bg-green-500"></div> </div> )} {title && ( <div className={cn( 'flex flex-row items-center gap-2', dots && 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2', )} > {icon ? ( <div className="text-muted-foreground [&_svg]:size-3.5" dangerouslySetInnerHTML={ typeof icon === 'string' ? { __html: icon } : undefined } > {typeof icon !== 'string' ? icon : null} </div> ) : null} <figcaption className="flex-1 truncate text-muted-foreground text-[13px]"> {title} </figcaption> </div> )} {copyButton ? ( <CopyButton content={code} size="sm" variant="ghost" className="-me-2 bg-transparent hover:bg-black/5 dark:hover:bg-white/10" onCopy={onCopy} /> ) : null} </div> ) : ( copyButton && ( <CopyButton content={code} size="sm" variant="ghost" className="absolute right-2 top-2 z-[2] backdrop-blur-md bg-transparent hover:bg-black/5 dark:hover:bg-white/10" onCopy={onCopy} /> ) )} <div ref={editorRef} className="h-[calc(100%-2.75rem)] w-full text-sm p-4 font-mono relative overflow-auto flex-1" > <div className={cn( '[&>pre,_&_code]:!bg-transparent [&>pre,_&_code]:[background:transparent_!important] [&>pre,_&_code]:border-none [&_code]:!text-[13px]', cursor && !isDone && "[&_.line:last-of-type::after]:content-['|'] [&_.line:last-of-type::after]:animate-pulse [&_.line:last-of-type::after]:inline-block [&_.line:last-of-type::after]:w-[1ch] [&_.line:last-of-type::after]:-translate-px", )} dangerouslySetInnerHTML={{ __html: highlightedCode }} /> </div> </div> );}export { CodeEditor, CopyButton, type CodeEditorProps, type CopyButtonProps };
No Writing Animation
import { CodeEditor } from '@/components/ui/shadcn-io/code-editor';import { Settings } from 'lucide-react';export const CodeEditorStaticDemo = () => { return ( <CodeEditor writing={false} className="w-[640px] h-[360px]" lang="javascript" title="config.js" icon={<Settings />} copyButton > {`/** @type {import('tailwindcss').Config} */module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))', background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', }, }, }, plugins: [],};`} </CodeEditor> );};export default CodeEditorStaticDemo;
'use client';import * as React from 'react';import { useInView, type UseInViewOptions } from 'motion/react';import { useTheme } from 'next-themes';import { cn } from '@/lib/utils';import { Button } from '@/components/ui/button';import { Copy, Check } from 'lucide-react';type CopyButtonProps = { content: string; size?: 'sm' | 'default' | 'lg'; variant?: 'default' | 'ghost' | 'outline'; className?: string; onCopy?: (content: string) => void;};function CopyButton({ content, size = 'default', variant = 'default', className, onCopy,}: CopyButtonProps) { const [copied, setCopied] = React.useState(false); const handleCopy = async () => { try { await navigator.clipboard.writeText(content); setCopied(true); onCopy?.(content); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy text: ', err); } }; return ( <Button size={size} variant={variant} onClick={handleCopy} className={cn('h-8 w-8 p-0', className)} > {copied ? ( <Check className="h-3 w-3" /> ) : ( <Copy className="h-3 w-3" /> )} </Button> );}type CodeEditorProps = Omit<React.ComponentProps<'div'>, 'onCopy'> & { children: string; lang: string; themes?: { light: string; dark: string; }; duration?: number; delay?: number; header?: boolean; dots?: boolean; icon?: React.ReactNode; cursor?: boolean; inView?: boolean; inViewMargin?: UseInViewOptions['margin']; inViewOnce?: boolean; copyButton?: boolean; writing?: boolean; title?: string; onDone?: () => void; onCopy?: (content: string) => void;};function CodeEditor({ children: code, lang, themes = { light: 'github-light', dark: 'github-dark', }, duration = 5, delay = 0, className, header = true, dots = true, icon, cursor = false, inView = false, inViewMargin = '0px', inViewOnce = true, copyButton = false, writing = true, title, onDone, onCopy, ...props}: CodeEditorProps) { const { resolvedTheme } = useTheme(); const editorRef = React.useRef<HTMLDivElement>(null); const [visibleCode, setVisibleCode] = React.useState(''); const [highlightedCode, setHighlightedCode] = React.useState(''); const [isDone, setIsDone] = React.useState(false); const inViewResult = useInView(editorRef, { once: inViewOnce, margin: inViewMargin, }); const isInView = !inView || inViewResult; React.useEffect(() => { if (!visibleCode.length || !isInView) return; const loadHighlightedCode = async () => { try { const { codeToHtml } = await import('shiki'); const highlighted = await codeToHtml(visibleCode, { lang, themes: { light: themes.light, dark: themes.dark, }, defaultColor: resolvedTheme === 'dark' ? 'dark' : 'light', }); setHighlightedCode(highlighted); } catch (e) { console.error(`Language "${lang}" could not be loaded.`, e); } }; loadHighlightedCode(); }, [ lang, themes, writing, isInView, duration, delay, visibleCode, resolvedTheme, ]); React.useEffect(() => { if (!writing) { setVisibleCode(code); onDone?.(); return; } if (!code.length || !isInView) return; const characters = Array.from(code); let index = 0; const totalDuration = duration * 1000; const interval = totalDuration / characters.length; let intervalId: NodeJS.Timeout; const timeout = setTimeout(() => { intervalId = setInterval(() => { if (index < characters.length) { setVisibleCode((prev) => { const currentIndex = index; index += 1; return prev + characters[currentIndex]; }); editorRef.current?.scrollTo({ top: editorRef.current?.scrollHeight, behavior: 'smooth', }); } else { clearInterval(intervalId); setIsDone(true); onDone?.(); } }, interval); }, delay * 1000); return () => { clearTimeout(timeout); clearInterval(intervalId); }; }, [code, duration, delay, isInView, writing, onDone]); return ( <div data-slot="code-editor" className={cn( 'relative bg-muted/50 w-[600px] h-[400px] border border-border overflow-hidden flex flex-col rounded-xl', className, )} {...props} > {header ? ( <div className="bg-muted border-b border-border/75 dark:border-border/50 relative flex flex-row items-center justify-between gap-y-2 h-10 px-4"> {dots && ( <div className="flex flex-row gap-x-2"> <div className="size-2 rounded-full bg-red-500"></div> <div className="size-2 rounded-full bg-yellow-500"></div> <div className="size-2 rounded-full bg-green-500"></div> </div> )} {title && ( <div className={cn( 'flex flex-row items-center gap-2', dots && 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2', )} > {icon ? ( <div className="text-muted-foreground [&_svg]:size-3.5" dangerouslySetInnerHTML={ typeof icon === 'string' ? { __html: icon } : undefined } > {typeof icon !== 'string' ? icon : null} </div> ) : null} <figcaption className="flex-1 truncate text-muted-foreground text-[13px]"> {title} </figcaption> </div> )} {copyButton ? ( <CopyButton content={code} size="sm" variant="ghost" className="-me-2 bg-transparent hover:bg-black/5 dark:hover:bg-white/10" onCopy={onCopy} /> ) : null} </div> ) : ( copyButton && ( <CopyButton content={code} size="sm" variant="ghost" className="absolute right-2 top-2 z-[2] backdrop-blur-md bg-transparent hover:bg-black/5 dark:hover:bg-white/10" onCopy={onCopy} /> ) )} <div ref={editorRef} className="h-[calc(100%-2.75rem)] w-full text-sm p-4 font-mono relative overflow-auto flex-1" > <div className={cn( '[&>pre,_&_code]:!bg-transparent [&>pre,_&_code]:[background:transparent_!important] [&>pre,_&_code]:border-none [&_code]:!text-[13px]', cursor && !isDone && "[&_.line:last-of-type::after]:content-['|'] [&_.line:last-of-type::after]:animate-pulse [&_.line:last-of-type::after]:inline-block [&_.line:last-of-type::after]:w-[1ch] [&_.line:last-of-type::after]:-translate-px", )} dangerouslySetInnerHTML={{ __html: highlightedCode }} /> </div> </div> );}export { CodeEditor, CopyButton, type CodeEditorProps, type CopyButtonProps };
Use Cases
- Documentation sites - Code examples with syntax highlighting and copy functionality
- Tutorial platforms - Step-by-step coding demonstrations with typing effects
- Portfolio showcases - Interactive code displays for developer presentations
- Demo applications - Code snippet previews with realistic animation timing
API Reference
CodeEditor
Prop | Type | Default | Description |
---|---|---|---|
children | string | required | The code content to display and animate |
lang | string | required | Programming language for syntax highlighting |
themes | { light: string; dark: string } | { light: 'github-light', dark: 'github-dark' } | Shiki theme names for light and dark modes |
duration | number | 5 | Animation duration in seconds for typing effect |
delay | number | 0 | Delay before animation starts in seconds |
header | boolean | true | Show macOS-style header with traffic lights |
dots | boolean | true | Show colored dots in header (red, yellow, green) |
icon | React.ReactNode | undefined | Icon to display next to title in header |
cursor | boolean | false | Show blinking cursor during typing animation |
inView | boolean | false | Only animate when component is in viewport |
inViewMargin | string | '0px' | Margin for viewport detection |
inViewOnce | boolean | true | Animate only once when entering viewport |
copyButton | boolean | false | Show copy to clipboard button |
writing | boolean | true | Enable typing animation effect |
title | string | undefined | Title text to display in header |
onDone | () => void | undefined | Callback when typing animation completes |
onCopy | (content: string) => void | undefined | Callback when copy button is clicked |
Implementation
Built with Shiki for syntax highlighting and Motion for animations. Uses next-themes for theme detection. Supports custom languages and themes. InView detection with configurable animation triggers.
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.
Code Tabs
Tabbed code display with syntax highlighting and copy functionality. Perfect for React applications requiring multi-variant code examples with Next.js integration and TypeScript support.