Animated Code Block
A code display component with real-time typing effects. Supports syntax highlighting and customizable themes.
Animated Code Block
Select a theme to customize the code display
Select Theme
javascript
1
2
3
4
5
6
7
8
9
10
11
Active theme: dark
Installation Guide
1
Install Dependencies
Framer Motion
npm install framer-motion
Lucide React
npm install lucide-react
Utility Functions
npm install clsx tailwind-merge
2
Setup Configuration
Create file:
/lib/utils.ts
/lib/utils.ts
Configuration1import { clsx, type ClassValue } from "clsx";
2import { twMerge } from "tailwind-merge";
3
4export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs));
6}
3
Copy Component Code
Animated Code Block.tsx
TypeScript1"use client";
2
3import { useState, useEffect, useRef } from "react";
4import { motion } from "framer-motion";
5import { cn } from "@/lib/utils";
6import { Play, Pause, Copy, Check, Terminal, RotateCcw } from "lucide-react";
7
8export interface AnimatedCodeBlockProps {
9 code: string;
10 language?: string;
11 theme?: "dark" | "light" | "terminal" | "cyberpunk" | "minimal" | "nuvyx";
12 typingSpeed?: number;
13 showLineNumbers?: boolean;
14 highlightLines?: number[];
15 title?: string;
16 className?: string;
17 autoPlay?: boolean;
18 loop?: boolean;
19 blurEffect?: boolean;
20 showControls?: boolean;
21 onCopy?: () => void;
22}
23
24export function AnimatedCodeBlock({
25 code,
26 language = "javascript",
27 theme = "dark",
28 typingSpeed = 50,
29 showLineNumbers = true,
30 highlightLines = [],
31 title,
32 className,
33 autoPlay = false,
34 loop = false,
35 blurEffect = false,
36 showControls = true,
37 onCopy,
38}: AnimatedCodeBlockProps) {
39 const [isPlaying, setIsPlaying] = useState(autoPlay);
40 const [currentPosition, setCurrentPosition] = useState(0);
41 const [copied, setCopied] = useState(false);
42 const [completed, setCompleted] = useState(false);
43 const [isPaused, setIsPaused] = useState(false);
44 const codeRef = useRef<HTMLDivElement>(null);
45 const timerRef = useRef<NodeJS.Timeout | null>(null);
46
47 const getThemeStyles = () => {
48 switch (theme) {
49 case "dark":
50 return {
51 background: "bg-gray-900",
52 text: "text-gray-100",
53 lineNumbers: "text-gray-500",
54 highlight: "bg-gray-800",
55 border: "border-gray-700",
56 };
57 case "minimal":
58 return {
59 background: "bg-yellow-100",
60 text: "text-gray-900",
61 lineNumbers: "text-gray-500",
62 highlight: "bg-yellow-200",
63 border: "border-yellow-300",
64 };
65 case "terminal":
66 return {
67 background: "bg-black",
68 text: "text-green-400",
69 lineNumbers: "text-green-700",
70 highlight: "bg-green-900/30",
71 border: "border-green-900",
72 };
73 case "cyberpunk":
74 return {
75 background: "bg-purple-950",
76 text: "text-pink-300",
77 lineNumbers: "text-purple-600",
78 highlight: "bg-pink-900/30",
79 border: "border-pink-700",
80 };
81 case "light":
82 return {
83 background: "bg-white",
84 text: "text-gray-800",
85 lineNumbers: "text-gray-400",
86 highlight: "bg-gray-200",
87 border: "border-gray-200",
88 };
89 case "nuvyx":
90 return {
91 background: "bg-black",
92 text: "text-purple-100",
93 lineNumbers: "text-purple-500",
94 highlight: "bg-purple-900/30",
95 border: "border-purple-700",
96 };
97 default:
98 return {
99 background: "bg-gray-900",
100 text: "text-gray-100",
101 lineNumbers: "text-gray-500",
102 highlight: "bg-gray-800",
103 border: "border-gray-700",
104 };
105 }
106 };
107
108 const themeStyles = getThemeStyles();
109
110 useEffect(() => {
111 if (isPlaying && currentPosition < code.length) {
112 timerRef.current = setTimeout(() => {
113 setCurrentPosition(currentPosition + 1);
114 }, typingSpeed);
115 } else if (isPlaying && currentPosition >= code.length) {
116 if (loop) {
117 setTimeout(() => {
118 setCurrentPosition(0);
119 }, 1000);
120 } else {
121 setIsPlaying(false);
122 setCompleted(true);
123 setIsPaused(false);
124 }
125 }
126
127 return () => {
128 if (timerRef.current) clearTimeout(timerRef.current);
129 };
130 }, [isPlaying, currentPosition, code, typingSpeed, loop]);
131
132 const togglePlay = () => {
133 if (isPlaying) {
134 setIsPaused(true);
135 } else if (completed) {
136 restartAnimation();
137 } else {
138 setIsPaused(false);
139 }
140 setIsPlaying(!isPlaying);
141 };
142
143 const restartAnimation = () => {
144 setCurrentPosition(0);
145 setIsPlaying(true);
146 setCompleted(false);
147 setIsPaused(false);
148 };
149
150 const copyCode = () => {
151 navigator.clipboard.writeText(code);
152 setCopied(true);
153 setTimeout(() => setCopied(false), 2000);
154 if (onCopy) onCopy();
155 };
156
157 const codeLines = code.split("\n");
158 const renderLines = () => {
159 let remainingChars = currentPosition;
160 const result = [];
161
162 for (let i = 0; i < codeLines.length; i++) {
163 const line = codeLines[i];
164 const lineLength = line.length + 1;
165
166 if (remainingChars <= 0) {
167 result.push("");
168 } else if (remainingChars >= lineLength) {
169 result.push(line);
170 remainingChars -= lineLength;
171 } else {
172 result.push(line.substring(0, remainingChars));
173 remainingChars = 0;
174 }
175 }
176
177 return result;
178 };
179
180 const displayedLines = completed ? code.split("\n") : renderLines();
181
182 const getCursorLineIndex = () => {
183 if (!isPlaying && !isPaused) return -1;
184
185 let charsProcessed = 0;
186 for (let i = 0; i < codeLines.length; i++) {
187 const lineLength = codeLines[i].length + 1;
188 charsProcessed += lineLength;
189
190 if (currentPosition < charsProcessed) {
191 return i;
192 }
193 }
194
195 return codeLines.length - 1;
196 };
197
198 const cursorLineIndex = getCursorLineIndex();
199
200 return (
201 <div
202 className={cn(
203 "animated-code-block rounded-lg overflow-hidden",
204 themeStyles.background,
205 themeStyles.text,
206 themeStyles.border,
207 "border",
208 className
209 )}
210 >
211 <div className="flex items-center justify-between p-2 border-b border-opacity-20">
212 <div className="flex items-center gap-2">
213 <Terminal size={16} />
214 <span className="text-sm font-medium">{title || language}</span>
215 </div>
216 {showControls && (
217 <div className="flex items-center gap-2">
218 {completed ? (
219 <button
220 onClick={restartAnimation}
221 className="p-1 rounded hover:bg-gray-700 hover:bg-opacity-30 transition-colors"
222 aria-label="Repeat animation"
223 >
224 <RotateCcw size={14} />
225 </button>
226 ) : (
227 <button
228 onClick={togglePlay}
229 className="p-1 rounded hover:bg-gray-700 hover:bg-opacity-30 transition-colors"
230 aria-label={isPlaying ? "Pause" : "Play"}
231 >
232 {isPlaying ? <Pause size={14} /> : <Play size={14} />}
233 </button>
234 )}
235 <button
236 onClick={copyCode}
237 className="p-1 rounded hover:bg-gray-700 hover:bg-opacity-30 transition-colors"
238 aria-label="Copy code"
239 >
240 {copied ? <Check size={14} /> : <Copy size={14} />}
241 </button>
242 </div>
243 )}
244 </div>
245
246 <div className="relative overflow-hidden">
247 {blurEffect && theme === "terminal" && (
248 <div className="absolute inset-0 pointer-events-none bg-green-400 opacity-[0.03] mix-blend-overlay" />
249 )}
250
251 <div className="overflow-x-auto">
252 <div className="flex min-w-full">
253 {showLineNumbers && (
254 <div
255 className={cn(
256 "text-xs py-4 px-2 text-right select-none",
257 themeStyles.lineNumbers
258 )}
259 >
260 {codeLines.map((_, i) => (
261 <div key={i} className="h-6 flex items-center justify-end">
262 {i + 1}
263 </div>
264 ))}
265 </div>
266 )}
267
268 <div className="relative py-4 flex-grow" ref={codeRef}>
269 {highlightLines.map((lineNum) => (
270 <div
271 key={`highlight-${lineNum}`}
272 className={cn(
273 "absolute left-0 right-0 h-6",
274 themeStyles.highlight
275 )}
276 style={{ top: `${(lineNum - 1) * 24 + 16}px` }}
277 />
278 ))}
279
280 <div className="relative z-10 px-4 font-mono text-sm">
281 {codeLines.map((line, i) => (
282 <div key={i} className="h-6 whitespace-pre">
283 {displayedLines[i] || ""}
284 {i === cursorLineIndex && (
285 <motion.span
286 className="inline-block w-2 h-5 bg-current -mb-0.5"
287 animate={{ opacity: [1, 0] }}
288 transition={{
289 repeat: Number.POSITIVE_INFINITY,
290 duration: 0.8,
291 }}
292 />
293 )}
294 </div>
295 ))}
296 </div>
297 </div>
298 </div>
299 </div>
300 </div>
301 </div>
302 );
303}
304
4
Final Steps
Update Import Paths
Make sure to update the import paths in the component code to match your project structure. For example, change @/components/ui/button
to match your UI components location.
Props
Name | Type | Default | Description |
---|---|---|---|
code | string | "" | The code to display and animate |
language | string | "javascript" | The programming language for syntax highlighting |
theme | string | dark | The visual theme of the code block (dark, light, terminal, cyberpunk, minimal, nuvyx) |
typingSpeed | number | 50 | The speed of the typing animation in milliseconds |
showLineNumbers | boolean | true | Whether to show line numbers |
highlightLines | number[] | [] | Array of line numbers to highlight |
title | string | "" | Title to display in the header |
autoPlay | boolean | false | Whether to start the animation automatically |
loop | boolean | false | Whether to loop the animation |
blurEffect | boolean | false | Whether to add a terminal blur effect |
showControls | boolean | true | Whether to show play/pause and copy controls |
onCopy | () => void | undefined | Callback function when code is copied |
className | string | "" | Additional CSS classes to apply |
Examples
Docker Multi-Stage Build
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Advanced Button Animation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17