HomeProjectsContact Animation System

Contact Page Animation System with VCard Export

Published Sep 19, 2025
ā‹…
Updated Jan 29, 2026
ā‹…
3 minutes read

Contact Page Animation System: From Individual Fields to Unified Export

Internal Development Case Study

The Problem

Leading up to 2025 MongoDB .local, I had a spare NFC chip available that was unlocked and had enough physical space to store a couple files/links and thats where the lightbulb struck.

"What if I am able to share my contact with anyone I end up speaking to throughout the course of the event? šŸ’”"

The goal was to create an engaging, saveable contact experience that would allow visitors to easily export contact information while providing visual feedback through smooth animations.

The challenge: design a system that could highlight individual contact fields, morph them into a unified visual representation, and export a properly formatted VCard, all while maintaining the established design system and animation patterns.

The Investigation

Initial requirements analysis revealed several UX considerations:

The existing pages followed and used Framer Motion with specific stagger patterns and Radix UI colors that were strictly layered with the core purpose of structure and standard, requiring this new contact page to integrate seamlessly with this baseline.

The Solution: Sequential Animation Architecture

Rather than static contact display, I built a progressive animation system that guides users through a "save contact" flow with visual storytelling at its core.

Core Animation Sequence

Individual Field Highlighting

const fieldSequence = ['name', 'location', 'email', 'phone', 'twitter', 'linkedin'];
 
for (let i = 0; i < fieldSequence.length; i++) {
  await new Promise(resolve => setTimeout(resolve, 300));
  setFields(prev => ({
    ...prev,
    [fieldSequence[i]]: true
  }));
}

Morphed Box Transition

{showMorphedBox && (
  <div className="absolute -top-12 inset-x-0 bottom-0 rounded border border-gray-6 animate-fadeInScale flex items-center justify-center">
    {saveStatus && (
      <div className="text-sm font-medium text-foreground bg-background px-3 py-1 rounded border border-gray-4">
        {saveStatus === "saving" ? "Saving..." : "Saved!"}
      </div>
    )}
  </div>
)}

State Management System

Multi-State Architecture

const [isAnimating, setIsAnimating] = useState(false);
const [showMorphedBox, setShowMorphedBox] = useState(false);
const [showEasterEgg, setShowEasterEgg] = useState(false);
const [saveStatus, setSaveStatus] = useState<"saving" | "saved" | null>(null);
const [fields, setFields] = useState({
  name: false,
  location: false,
  email: false,
  phone: false,
  twitter: false,
  linkedin: false,
});

Hurdle #1: CSS Positioning Conflicts

The Problem: Individual field highlights and the morphed box were creating visual overlaps due to Tailwind's important: true configuration and article CSS applying margin-top: 24px to paragraphs.

Investigation:

/* In styles/main.css */
article {
  p:not(:first-child) {
    margin-top: 24px; /* Conflicting with absolute positioning */
  }
}

Solution: Implemented conditional opacity transitions instead of complex positioning logic:

className={`transition-all duration-500 ${
  fields.location && !showMorphedBox ? "rounded border border-gray-6 opacity-100" : 
  showMorphedBox ? "opacity-0" : "opacity-100"
}`}

Hurdle #2: Animation Timing Conflicts

The Problem: Individual highlights and the morphed box were appearing simultaneously, creating weird visual stacking effects.

Investigation: The original timing was too aggressive:

// Original - caused overlaps
await new Promise(resolve => setTimeout(resolve, 500));
setShowMorphedBox(true);

Solution: Refined timing sequence with proper fade-out coordination:

// Refined timing prevents overlap
await new Promise(resolve => setTimeout(resolve, 300));
setShowMorphedBox(true);
setSaveStatus("saving");

Added custom Tailwind animation for smooth morphed box entrance:

// tailwind.config.ts
fadeInScale: {
  "0%": { opacity: "0", transform: "scale(0.95)" },
  "100%": { opacity: "1", transform: "scale(1)" },
}

Hurdle #3: VCard Format Compatibility

The Problem: Generated VCard files weren't importing correctly into contacts applications.

Investigation: Initial VCard had formatting issues:

// Problematic format
let vcard = "BEGIN:VCARD\nVERSION:3.0\n";
if (fields.phone) vcard += "TEL:+1 (234) 567-890\n"; // Wrong phone number

Solution: Implemented proper RFC 2426 VCard standards:

const generateVCard = () => {
  let vcard = "BEGIN:VCARD\r\nVERSION:3.0\r\n";
  
  // Always include name since it's the primary contact
  vcard += "FN:Vansh Bataviya\r\n";
  vcard += "N:Bataviya;Vansh;;;\r\n";
  
  // Proper phone format without spaces/parentheses
  vcard += "TEL:+13157422204\r\n";
  
  vcard += "END:VCARD\r\n";
  return vcard;
};

Key fixes:

Hurdle #4: Button State Management

The Problem: The "Save Contact" button remained visible while the save status was shown on the morphed box, creating redundant UI elements.

Solution: Implemented coordinated fade-out of button when morphed box appears:

className={`rounded px-3 py-2 text-xs transition-all duration-500 ${
  showMorphedBox ? "opacity-0" : "opacity-100"
}`}

This creates a clean handoff where the button's functionality is visually transferred to the morphed box status display.

The Final Implementation

Animation Flow

  1. User clicks "Save Contact" → Button becomes "Saving Contact..."
  2. Sequential highlighting → Individual fields highlight top to bottom (300ms intervals)
  3. Morphed box appears → Large box eases in while individuals fade out
  4. Status progression → "Saving..." changes to "Saved!"
  5. Download trigger → VCard automatically downloads
  6. Easter egg → "(not savable to contacts)" appears next to Resume
  7. Reset → All states return to initial values

Performance Optimizations

Lessons Learned

State Coordination: Multi-state animations require timing orchestration. Using async/await with setTimeout provides with more predictable sequencing whilst maintaining readability(for future self).

VCard Standards: Contact export formats require some awareness of RFC specifications existing. Small formatting details along the development of this contacts export(line endings, field structure) significantly impacted compatibility in initial iterations with export.

Visual Continuity: Smooth state transitions prevent jarring UI changes. Coordinating element visibility ensures users never see conflicting or overlapping states.

CSS Specificity: Given another attempt to build more custom one off interactions, Tailwind's important: true configuration came with conflicts as anticipated.

The contact animation system successfully transforms a static information page into an engaging, functional interface that guides users through contact saving while maintaining the portfolio's established design language and performance standards.