
Crafting an Animated Link Component with react-rough-notation in Next.js
Building an Animated Link Component with react-rough-notation in Next.js
Subtle animations can transform a mundane navigation experience into something memorable. In this guide, I'll walk you through building an animated link component that uses hand-drawn annotations to add personality and interactivity to your Next.js application.
What We're Building
An animated link component that displays playful, hand-drawn annotations on hover. The component features:
- Beautiful hand-drawn animations on hover
- Fully responsive and mobile-friendly design
- Active link state handling with visual feedback
- Multiple annotation styles (underline, box, circle, highlight, and more)
- Fully customizable colors and animation properties
- Seamless integration with Next.js and Tailwind CSS
- Written in TypeScript for better development experience
Prerequisites
Before we start, ensure you have:
- A Next.js project (version 13+ recommended)
- Basic familiarity with React hooks and TypeScript
- Tailwind CSS configured in your project
Installation
First, install the required dependencies:
npm install react-rough-notation clsx tailwind-merge
These packages provide:
react-rough-notation: The core library for creating hand-drawn annotationsclsx: Utility for constructing className strings conditionallytailwind-merge: Intelligently merges Tailwind CSS classes
Setting Up the Utility Function
Create a utility function for merging class names. This is essential for handling Tailwind CSS classes properly.
Create lib/utils.ts:
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
This utility function combines clsx for conditional classes and twMerge to handle Tailwind CSS class conflicts intelligently.
Component Implementation
Now let's build the core component. Create components/annotated-link.tsx:
'use client'
import { RoughNotation } from 'react-rough-notation';
import Link from 'next/link';
import { useState } from 'react';
import { usePathname } from 'next/navigation';
import { cn } from "@/lib/utils";
type AnnotationType =
| "underline"
| "box"
| "circle"
| "highlight"
| "strike-through"
| "crossed-off"
| "bracket";
interface AnnotatedLinkProps {
text?: string;
href: string;
type?: AnnotationType;
className?: string;
activeClassName?: string;
inactiveClassName?: string;
color?: string;
strokeWidth?: number;
animate?: boolean;
iterations?: number;
padding?: number;
children?: React.ReactNode;
}
const AnnotatedLink: React.FC<AnnotatedLinkProps> = ({
text,
href,
type = "underline",
className,
activeClassName = "text-accent-lighter underline underline-offset-4 decoration-2",
inactiveClassName = "text-white",
color = "#9E86BA",
strokeWidth = 2,
animate = true,
iterations = 2,
padding = 2,
children
}) => {
const [isAnnotationVisible, setAnnotationVisible] = useState<boolean>(false);
const path = usePathname();
const isActiveLink = path?.includes(text?.toLowerCase() ?? '');
const handleMouseEnter = (): void => {
setAnnotationVisible(true);
};
const handleMouseLeave = (): void => {
setAnnotationVisible(false);
};
return (
<RoughNotation
type={type}
strokeWidth={strokeWidth}
show={isAnnotationVisible}
color={color}
animate={animate}
iterations={iterations}
padding={padding}
>
<Link
className={cn(
className,
isActiveLink ? activeClassName : inactiveClassName
)}
href={href}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children || text}
</Link>
</RoughNotation>
);
};
export default AnnotatedLink;
Component Breakdown
Let's understand the key parts:
State Management:
const [isAnnotationVisible, setAnnotationVisible] = useState<boolean>(false);
Controls when the annotation appears based on hover state.
Active Link Detection:
const path = usePathname();
const isActiveLink = path?.includes(text?.toLowerCase() ?? '');
Uses Next.js's usePathname hook to determine if the current route matches the link destination.
Event Handlers:
const handleMouseEnter = (): void => {
setAnnotationVisible(true);
};
const handleMouseLeave = (): void => {
setAnnotationVisible(false);
};
Toggle annotation visibility on hover events.
Usage Examples
Basic Navigation
import { AnnotatedLink } from "@/components/annotated-link"
export default function Navigation() {
return (
<nav className="flex gap-6">
<AnnotatedLink href="/about" text="About" />
<AnnotatedLink href="/projects" text="Projects" />
<AnnotatedLink href="/blog" text="Blog" />
<AnnotatedLink href="/contact" text="Contact" />
</nav>
)
}
Custom Styling
Apply custom styles and annotation types:
<AnnotatedLink
href="/projects"
text="View Projects"
type="box"
color="#FF5733"
className="font-bold text-lg hover:text-blue-500"
activeClassName="text-blue-600 font-semibold"
inactiveClassName="text-gray-800"
strokeWidth={3}
iterations={3}
padding={4}
/>
With Custom Content
Use children instead of text for complex content:
<AnnotatedLink href="/github" type="circle" color="#333">
<div className="flex items-center gap-2">
<GithubIcon className="w-5 h-5" />
<span>Follow me on GitHub</span>
</div>
</AnnotatedLink>
Different Annotation Styles
{/* Underline effect */}
<AnnotatedLink href="/about" text="About" type="underline" />
{/* Box around text */}
<AnnotatedLink href="/work" text="Work" type="box" color="#10b981" />
{/* Circle annotation */}
<AnnotatedLink href="/contact" text="Contact" type="circle" color="#3b82f6" />
{/* Highlight effect */}
<AnnotatedLink href="/blog" text="Blog" type="highlight" color="#fbbf24" />
Component API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
text |
string |
- | Optional text content of the link |
href |
string |
Required | Link destination URL |
type |
AnnotationType |
"underline" |
Type of annotation effect |
className |
string |
- | Additional CSS classes for the link |
activeClassName |
string |
"text-accent-lighter underline..." |
Classes applied when link is active |
inactiveClassName |
string |
"text-white" |
Classes applied when link is inactive |
color |
string |
"#9E86BA" |
Color of the annotation stroke |
strokeWidth |
number |
2 |
Width of the annotation stroke in pixels |
animate |
boolean |
true |
Whether to animate the annotation appearance |
iterations |
number |
2 |
Number of times to draw the annotation |
padding |
number |
2 |
Padding around the annotation in pixels |
children |
ReactNode |
- | Optional child elements (overrides text) |
Annotation Types
The component supports seven distinct annotation styles:
underline- Clean underline beneath the textbox- Rectangle drawn around the textcircle- Circular shape enclosing the texthighlight- Highlighter marker effectstrike-through- Line through the textcrossed-off- Diagonal cross over the textbracket- Bracket notation around the text
Advanced Customization
Responsive Design
Use Tailwind's responsive utilities for different screen sizes:
<AnnotatedLink
href="/projects"
text="Projects"
className="text-sm md:text-base lg:text-lg px-2 md:px-4"
strokeWidth={2}
padding={3}
/>
Dark Mode Support
Leverage CSS variables for theme-aware colors:
<AnnotatedLink
href="/about"
text="About"
className="text-gray-800 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
color="var(--annotation-color)"
/>
Add to your CSS:
:root {
--annotation-color: #9E86BA;
}
.dark {
--annotation-color: #B4A0C8;
}
Dynamic Colors
Create variants for different contexts:
// Primary navigation
<AnnotatedLink
href="/home"
text="Home"
color="#3b82f6"
type="underline"
/>
// Call-to-action
<AnnotatedLink
href="/contact"
text="Get in Touch"
color="#ef4444"
type="box"
strokeWidth={3}
className="font-semibold"
/>
// Subtle secondary links
<AnnotatedLink
href="/privacy"
text="Privacy"
color="#6b7280"
type="underline"
strokeWidth={1}
iterations={1}
/>
Best Practices
1. Accessibility
The component inherits Next.js Link's accessibility features, but ensure you maintain proper contrast ratios:
// Good contrast
<AnnotatedLink
href="/about"
text="About"
className="text-gray-900"
color="#3b82f6"
/>
// Avoid low contrast combinations
2. Performance
Annotations are only rendered on hover, minimizing performance impact:
const [isAnnotationVisible, setAnnotationVisible] = useState<boolean>(false);
// Annotation only shows when isAnnotationVisible is true
3. Animation Timing
Adjust iterations and animation speed for optimal feel:
// Fast, subtle animation
<AnnotatedLink iterations={1} animate={true} />
// Slower, more deliberate
<AnnotatedLink iterations={3} animate={true} />
// Instant (no animation)
<AnnotatedLink animate={false} />
4. Mobile Considerations
Since mobile devices don't have hover states, consider showing annotations differently:
<AnnotatedLink
href="/contact"
text="Contact"
// Mobile users see the active state immediately
className="md:hover:text-blue-500"
/>
Common Use Cases
Navigation Menu
export default function Header() {
return (
<header className="py-6 px-8">
<nav className="flex gap-8 items-center">
<AnnotatedLink href="/" text="Home" type="underline" />
<AnnotatedLink href="/about" text="About" type="underline" />
<AnnotatedLink href="/work" text="Work" type="box" />
<AnnotatedLink
href="/contact"
text="Contact"
type="highlight"
color="#22c55e"
className="font-semibold"
/>
</nav>
</header>
)
}
Footer Links
export default function Footer() {
return (
<footer className="py-12 px-8 bg-gray-900">
<div className="grid grid-cols-3 gap-8">
<div>
<h3 className="text-white font-bold mb-4">Quick Links</h3>
<div className="flex flex-col gap-2">
<AnnotatedLink
href="/privacy"
text="Privacy Policy"
color="#9ca3af"
inactiveClassName="text-gray-400"
activeClassName="text-white"
/>
<AnnotatedLink
href="/terms"
text="Terms of Service"
color="#9ca3af"
inactiveClassName="text-gray-400"
activeClassName="text-white"
/>
</div>
</div>
</div>
</footer>
)
}
Call-to-Action Button
<AnnotatedLink
href="/get-started"
type="box"
color="#10b981"
strokeWidth={3}
iterations={2}
padding={8}
className="inline-block px-8 py-4 text-lg font-bold"
>
Get Started Today
</AnnotatedLink>
Troubleshooting
Annotation not appearing
Problem: The annotation doesn't show on hover.
Solution: Ensure you have the 'use client' directive at the top of the component file:
'use client'
import { RoughNotation } from 'react-rough-notation';
Active state not working
Problem: The active link detection isn't working correctly.
Solution: Verify your routing structure matches the text prop:
// If your route is /about-us
// Make sure text matches
<AnnotatedLink href="/about-us" text="about-us" />
Styling conflicts
Problem: Custom styles aren't being applied.
Solution: Use the cn utility and check class specificity:
import { cn } from "@/lib/utils"
<AnnotatedLink
className={cn("your-classes", "more-classes")}
/>
Conclusion
The AnnotatedLink component demonstrates how small interactive details can significantly enhance user experience. By combining react-rough-notation with Next.js routing and Tailwind CSS, we've created a flexible, reusable component that adds personality to navigation elements.
The hand-drawn aesthetic creates a friendly, approachable feel while maintaining professional functionality. Whether you're building a portfolio, a blog, or a marketing site, this component can help make your navigation more engaging and memorable.
Next Steps
- Experiment with different annotation types to match your brand
- Create variants for different contexts (primary nav, footer, CTAs)
- Add motion preferences detection for users who prefer reduced motion
- Extend with additional features like external link indicators
Resources
- react-rough-notation Documentation
- Next.js Link Component
- Tailwind CSS Documentation
- TypeScript React Cheatsheet
Feel free to customize this component to match your design system and creative vision. The beauty of hand-drawn annotations is that they're inherently unique and personal.