Building a Personal Touch - How I Added Typewriter Effects and Smooth Animations to My Blog
7 min read
0
The Problem: Too Generic, Too AI-like
My blog started with a generic name - "Tech Blog". It worked, but it lacked personality. It felt like every other tech blog out there, generated by AI without a human touch. I wanted something more authentic, something that represented me as a developer.
The Solution: Three Key Improvements
1. Personal Branding with "Leon's Insights"
Instead of "Tech Blog", I rebranded to "Leon's Insights" - a name that:
- ✅ Uses my actual surname (Leon)
- ✅ Reflects the purpose (documenting my coding journey)
- ✅ Feels authentic and personal
- ✅ Is memorable and unique
2. Dynamic Typewriter Effect
Added a rotating typewriter effect showing different aspects of my identity:
- Developer
- Problem Solver
- Lifelong Learner
- Code Creator
This creates visual interest and communicates my values without being static.
3. Smooth Page Transitions & Animations
Enhanced user experience with:
- Smooth page load animations
- Hover effects on stats cards
- Gradient backgrounds
- Glass-morphism effects
All optimized for performance (60fps+).
Implementation Guide
Step 1: Create the Typewriter Component
I built a reusable TypeScript component with two modes:
components/typewriter-text.tsx
'use client'
import { useEffect, useState } from 'react'
export function TypewriterLoop({
texts,
typingDelay = 100,
deletingDelay = 50,
pauseDelay = 2000,
className = ''
}: TypewriterLoopProps) {
const [displayText, setDisplayText] = useState('')
const [textIndex, setTextIndex] = useState(0)
const [isDeleting, setIsDeleting] = useState(false)
const [isPaused, setIsPaused] = useState(false)
useEffect(() => {
const currentText = texts[textIndex] || ''
// Handle pausing after completing a word
if (isPaused) {
const timeout = setTimeout(() => {
setIsPaused(false)
setIsDeleting(true)
}, pauseDelay)
return () => clearTimeout(timeout)
}
// Check if we've completed typing the current word
if (!isDeleting && displayText === currentText) {
setIsPaused(true)
return
}
// Check if we've finished deleting
if (isDeleting && displayText === '') {
setIsDeleting(false)
setTextIndex((prev) => (prev + 1) % texts.length)
return
}
// Type or delete one character
const timeout = setTimeout(
() => {
setDisplayText((prev) =>
isDeleting
? currentText.substring(0, prev.length - 1)
: currentText.substring(0, prev.length + 1)
)
},
isDeleting ? deletingDelay : typingDelay
)
return () => clearTimeout(timeout)
}, [displayText, textIndex, isDeleting, isPaused, texts, typingDelay, deletingDelay, pauseDelay])
return (
<span className={className}>
{displayText}
<span className="animate-pulse text-indigo-500">|</span>
</span>
)
}Key Features:
- ⚡ Performance-optimized: Uses single
setTimeoutinstead ofsetInterval - 🎯 TypeScript: Full type safety
- 🔄 Loop support: Automatically cycles through multiple texts
- ⏸️ Configurable delays: Control typing speed, deletion speed, and pause duration
- 💫 Animated cursor: Blinking cursor using CSS
animate-pulse
Step 2: Create a Client Component Wrapper
Since Next.js 14 uses Server Components by default, I created a wrapper:
components/hero-typewriter.tsx
'use client'
import { TypewriterLoop } from './typewriter-text'
interface HeroTypewriterProps {
texts: string[]
locale: 'en' | 'zh'
}
export function HeroTypewriter({ texts, locale }: HeroTypewriterProps) {
return (
<div className="h-12 flex items-center justify-center">
<div className="text-xl sm:text-2xl font-medium text-muted-foreground">
<TypewriterLoop
texts={texts}
typingDelay={120}
deletingDelay={80}
pauseDelay={2000}
/>
</div>
</div>
)
}This keeps the server component benefits while allowing client-side interactivity.
Step 3: Update the Homepage
app/[locale]/page.tsx
export default async function HomePage({ params }: Props) {
const { locale } = await params
const typewriterTexts = ['Developer', 'Problem Solver', 'Lifelong Learner', 'Code Creator']
return (
<main className="page-content">
<section className="relative overflow-hidden py-20 sm:py-28">
<div className="absolute inset-0 bg-gradient-to-b from-primary/5 via-transparent to-transparent pointer-events-none" />
<div className="mx-auto max-w-6xl px-4 sm:px-6 relative">
<div className="text-center">
{/* Animated badge */}
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass-card text-sm font-medium mb-8 animate-float">
<Sparkles className="h-4 w-4 text-primary" />
<span className="bg-gradient-to-r from-primary to-purple-600 bg-clip-text text-transparent">
✨ Welcome to Leon's Insights
</span>
</div>
{/* Main title */}
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight mb-4">
<span className="gradient-text">
Leon's Insights
</span>
</h1>
{/* Typewriter subtitle */}
<HeroTypewriter texts={typewriterTexts} locale={locale} />
{/* Description */}
<p className="mt-6 text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
Documenting my journey through code, one commit at a time. Sharing insights on web development, DevOps, and everything in between.
</p>
{/* CTA Buttons */}
<div className="mt-10 flex flex-wrap items-center justify-center gap-4">
<Link
href={`/${locale}/posts`}
className="group inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-gradient-to-r from-primary to-purple-600 text-white font-medium shadow-lg shadow-primary/25 hover:shadow-primary/40 hover:scale-105 transition-all duration-300"
>
<BookOpen className="h-5 w-5 group-hover:rotate-12 transition-transform" />
Browse Articles
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
{/* Animated Stats Cards */}
<div className="mt-20 grid grid-cols-3 gap-6 max-w-2xl mx-auto">
<div className="glass-card rounded-2xl p-6 text-center group hover:scale-105 hover:shadow-lg hover:shadow-primary/10 transition-all duration-300">
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500/10 to-blue-600/10 mb-3 group-hover:scale-110 transition-transform">
<Code2 className="h-6 w-6 text-blue-500" />
</div>
<div className="text-3xl font-bold bg-gradient-to-r from-blue-500 to-blue-600 bg-clip-text text-transparent">
{stats.posts}
</div>
<div className="text-sm text-muted-foreground mt-1">
Articles
</div>
</div>
{/* More stat cards... */}
</div>
</div>
</section>
</main>
)
}Step 4: Update i18n Messages
messages/en.json
{
"HomePage": {
"title": "Leon's Insights",
"subtitle": "Data, Logic, and Digital Life",
"description": "Sharing insights on technology, problem-solving, and continuous learning. Exploring data science, software engineering, and digital innovation."
}
}messages/zh.json
{
"HomePage": {
"title": "Leon's Insights (Chinese)",
"subtitle": "Data, Logic, and Digital Life",
"description": "Localized Chinese copy goes here for the HomePage description."
}
}Performance Optimizations
1. CSS-based Animations
Used CSS transform and opacity instead of left/top for 60fps animations:
.glass-card {
transition: transform 0.3s ease-out, box-shadow 0.3s ease-out;
}
.glass-card:hover {
transform: scale(1.05);
box-shadow: 0 20px 40px rgba(99, 102, 241, 0.1);
}2. Hardware Acceleration
Triggered GPU acceleration with will-change:
.animate-float {
animation: float 3s ease-in-out infinite;
will-change: transform;
}3. Debounced Typewriter
Used setTimeout cleanup to prevent memory leaks:
useEffect(() => {
const timeout = setTimeout(() => {
// Animation logic
}, delay)
return () => clearTimeout(timeout) // Cleanup!
}, [dependencies])4. Server Component by Default
Only the typewriter effect runs on the client. Everything else is server-rendered for fast initial load.
Results
Before vs After
Before:
- Generic "Tech Blog" title
- Static content
- Basic styling
- No personality
After:
- Personal "Leon's Insights" branding
- Dynamic typewriter effect
- Smooth animations & hover effects
- Authentic, human touch
Performance Metrics
- ✅ First Contentful Paint (FCP): < 1.2s
- ✅ Largest Contentful Paint (LCP): < 2.5s
- ✅ Cumulative Layout Shift (CLS): < 0.1
- ✅ Animation Frame Rate: 60fps
All animations use transform and opacity for optimal performance.
Key Takeaways
-
Personal branding matters: Don't settle for generic names. Use your identity.
-
Typewriter effects are powerful: They create visual interest and communicate multiple messages in limited space.
-
Performance first: Always optimize animations for 60fps using CSS transforms.
-
Server + Client balance: Use Server Components for static content, Client Components only where needed.
-
Accessibility: Ensure animations don't cause motion sickness (respect
prefers-reduced-motion).
Future Enhancements
- Add cursor blink animation customization
- Support for multi-line typewriter effect
- Pause on hover feature
- Analytics for which subtitle gets the most engagement
- Sound effects on typing (optional)
Conclusion
Transforming my blog from a generic "Tech Blog" to "Leon's Insights" was about more than just changing a name. It was about adding personality, creating engaging interactions, and optimizing for performance.
The typewriter effect is a perfect example of how small UI details can make a big difference. It's dynamic, eye-catching, and communicates multiple aspects of my identity without overwhelming the user.
Try it yourself! The code is open-source and fully TypeScript. Feel free to adapt it for your own projects.
Tech Stack:
- Next.js 14 (App Router)
- TypeScript 5
- Tailwind CSS
- React Hooks
- CSS Animations
Performance:
- 60fps animations
- Hardware-accelerated transforms
- Optimized re-renders
- Server-first rendering
Repository: GitHub - Leon's Insights
Built with ❤️ by Leon | January 31, 2026