DocumentationSocial Flip Button

Social Flip Button

A social media button group that flips letters to reveal icons on hover, featuring animated border lines.

Install using CLI

npx shadcn@latest add "https://vengeance-ui.vercel.app/r/social-flip-button.json"

Install Manually

1

Install dependencies

npm install framer-motion clsx tailwind-merge react-icons
2

Add util file

lib/utils.ts

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3

Add styles to global CSS

Add the following styles to your app/globals.css file

/* Glass Component Borders */
.glass-border {
border: 1px solid rgba(0, 0, 0, 0.15);
}
.dark .glass-border {
border: 1px solid rgba(255, 255, 255, 0.2);
}
4

Copy the source code

Copy the code below and paste it into components/ui/social-flip-button.tsx

"use client";
import { motion, AnimatePresence } from "framer-motion";
import React, { useState } from "react";
import { cn } from "@/lib/utils";
import {
FaGithub,
FaTwitter,
FaFacebook,
FaInstagram,
FaLinkedin,
FaEnvelope,
FaDiscord,
} from "react-icons/fa";
export interface SocialItem {
letter: string;
icon: React.ReactNode;
label: string;
href?: string;
onClick?: () => void;
}
interface SocialFlipButtonProps {
items?: SocialItem[];
className?: string;
itemClassName?: string;
frontClassName?: string;
backClassName?: string;
}
const defaultItems: SocialItem[] = [
{ letter: "C", icon: <FaGithub />, label: "Github", href: "#" },
{ letter: "O", icon: <FaTwitter />, label: "Twitter", href: "#" },
{ letter: "N", icon: <FaLinkedin />, label: "LinkedIn", href: "#" },
{ letter: "T", icon: <FaInstagram />, label: "Instagram", href: "#" },
{ letter: "A", icon: <FaFacebook />, label: "Facebook", href: "#" },
{ letter: "C", icon: <FaEnvelope />, label: "Email", href: "#" },
{ letter: "T", icon: <FaDiscord />, label: "Discord", href: "#" },
];
const SocialFlipNode = ({
item,
index,
isHovered,
setTooltipIndex,
tooltipIndex,
itemClassName,
frontClassName,
backClassName,
}: {
item: SocialItem;
index: number;
isHovered: boolean;
setTooltipIndex: (val: number | null) => void;
tooltipIndex: number | null;
itemClassName?: string;
frontClassName?: string;
backClassName?: string;
}) => {
const Wrapper = item.href ? "a" : "div";
const wrapperProps = item.href
? { href: item.href, target: "_blank", rel: "noopener noreferrer" }
: { onClick: item.onClick };
return (
<Wrapper
{...wrapperProps}
className={cn("relative h-10 w-10 cursor-pointer", itemClassName)}
style={{ perspective: "1000px" }}
onMouseEnter={() => setTooltipIndex(index)}
onMouseLeave={() => setTooltipIndex(null)}
>
<AnimatePresence>
{isHovered && tooltipIndex === index && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.8, x: "-50%" }}
animate={{ opacity: 1, y: -50, scale: 1, x: "-50%" }}
exit={{ opacity: 0, y: 10, scale: 0.8, x: "-50%" }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="absolute left-1/2 z-50 whitespace-nowrap rounded-lg bg-neutral-900 px-3 py-1.5 text-xs font-semibold text-white shadow-xl dark:bg-white dark:text-neutral-900"
>
{item.label}
{/* Arrow */}
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2 h-2 w-2 rotate-45 bg-neutral-900 dark:bg-white" />
</motion.div>
)}
</AnimatePresence>
<motion.div
className="relative h-full w-full"
initial={false}
animate={{ rotateY: isHovered ? 180 : 0 }}
transition={{
duration: 0.8,
type: "spring",
stiffness: 120,
damping: 15,
delay: index * 0.08,
}}
style={{ transformStyle: "preserve-3d" }}
>
{/* Front - Letter */}
<div
className={cn(
"absolute inset-0 flex items-center justify-center rounded-lg bg-neutral-100 text-lg font-bold text-neutral-800 shadow-sm dark:bg-neutral-900 dark:text-neutral-200",
frontClassName
)}
style={{ backfaceVisibility: "hidden" }}
>
{item.letter}
</div>
{/* Back - Icon */}
<div
className={cn(
"absolute inset-0 flex items-center justify-center rounded-lg bg-black text-lg text-white dark:bg-white dark:text-black",
backClassName
)}
style={{
backfaceVisibility: "hidden",
transform: "rotateY(180deg)",
}}
>
{item.icon}
</div>
</motion.div>
</Wrapper>
);
};
export default function SocialFlipButton({
items = defaultItems,
className,
itemClassName,
frontClassName,
backClassName,
}: SocialFlipButtonProps) {
const [isHovered, setIsHovered] = useState(false);
const [tooltipIndex, setTooltipIndex] = useState<number | null>(null);
const [isDark, setIsDark] = useState(false);
React.useEffect(() => {
const checkTheme = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
checkTheme();
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
}, []);
return (
<div className={cn("flex items-center justify-center gap-4 p-4", className)}>
<div
className="group relative flex items-center justify-center gap-2 rounded-2xl glass-border bg-white p-4 shadow-sm dark:bg-black"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setIsHovered(false);
setTooltipIndex(null);
}}
>
{/* Border Lines Container - Clipped */}
<div className="absolute -inset-[1px] overflow-hidden rounded-2xl pointer-events-none">
{/* Animated Top Border Line */}
<motion.div
className="absolute top-0 left-0 h-[1px] w-full bg-gradient-to-r from-transparent via-black/50 to-transparent dark:via-white/50"
animate={{ x: ["-100%", "100%"] }}
transition={{
duration: 2.5,
repeat: Infinity,
ease: "linear",
}}
/>
{/* Animated Bottom Border Line */}
<motion.div
className="absolute bottom-0 left-0 h-[1px] w-full bg-gradient-to-r from-transparent via-black/50 to-transparent dark:via-white/50"
animate={{ x: ["100%", "-100%"] }}
transition={{
duration: 2.5,
repeat: Infinity,
ease: "linear",
}}
/>
</div>
{items.map((item, index) => (
<SocialFlipNode
key={index}
item={item}
index={index}
isHovered={isHovered}
setTooltipIndex={setTooltipIndex}
tooltipIndex={tooltipIndex}
itemClassName={itemClassName}
frontClassName={frontClassName}
backClassName={backClassName}
/>
})};
</div>
</div>
);
}

Usage

1import SocialFlipButton from "@/components/ui/social-flip-button"
2
3export function SocialFlipButtonDemo() {
4return (
5 <div className="flex items-center justify-center h-[300px] w-full">
6 <SocialFlipButton />
7 </div>
8);
9}

Props

Prop NameTypeDefaultDescription
itemsSocialItem[]defaultItemsArray of social items with letter, icon, label, and href/onClick.
classNamestring-Class for the outer wrapper.
itemClassNamestring-Class for the individual button items.
frontClassNamestring-Class for the front face (content side).
backClassNamestring-Class for the back face (icon side).