Crafting an Animated Link Component with react-rough-notation in Next.js
Next.jsTypeScriptReactUI ComponentsTailwind CSSAnimation

Crafting an Animated Link Component with react-rough-notation in Next.js

8 min read

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 annotations
  • clsx: Utility for constructing className strings conditionally
  • tailwind-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 text
  • box - Rectangle drawn around the text
  • circle - Circular shape enclosing the text
  • highlight - Highlighter marker effect
  • strike-through - Line through the text
  • crossed-off - Diagonal cross over the text
  • bracket - 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


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.