Building an Animated Social Media Dock Component with React and Framer Motion

Manish Tamang

Manish Tamang / January 21, 2025

6 min readNaN views

Building an Animated Social Media Dock Component

Create an engaging macOS-style dock animation for your social media icons using React, Framer Motion, and Tailwind CSS. This tutorial will walk you through building a smooth, interactive dock component that magnifies icons on hover.

Demo Preview

Prerequisites

Before we begin, make sure you have a React project set up with TypeScript and Tailwind CSS.

Installation

First, install the required dependencies:


npm install tailwindcss@latest clsx tailwind-merge framer-motion

Setup

Create a utility file for class name management. Create lib/utils.ts in your project root:


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

Creating the Dock Component

Create a new file components/dock.tsx. This will contain our core animation logic:


"use client";
import { cn } from "@/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import React, { PropsWithChildren, useRef } from "react";
// Default animation values
const DEFAULT_MAGNIFICATION = 60;
const DEFAULT_DISTANCE = 140;
// Component interfaces
export interface DockProps extends VariantProps<typeof dockVariants> {
className?: string;
magnification?: number;
distance?: number;
children: React.ReactNode;
}
// Dock styling variants
const dockVariants = cva(
"mx-auto w-max mt-8 h-[58px] p-2 flex items-end gap-2 rounded-2xl border dark:border-[#707070]",
);
// Dock Component
const Dock = React.forwardRef<HTMLDivElement, DockProps>(
(
{
className,
children,
magnification = DEFAULT_MAGNIFICATION,
distance = DEFAULT_DISTANCE,
...props
},
ref,
) => {
const mouseX = useMotionValue(Infinity);
const renderChildren = () => {
return React.Children.map(children, (child: any) => {
return React.cloneElement(child, {
mouseX: mouseX,
magnification: magnification,
distance: distance,
});
});
};
return (
<motion.div
ref={ref}
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
{...props}
className={cn(dockVariants({ className }), className)}
>
{renderChildren()}
</motion.div>
);
},
);
Dock.displayName = "Dock";
// DockIcon Component Interface
export interface DockIconProps {
size?: number;
magnification?: number;
distance?: number;
mouseX?: any;
className?: string;
children?: React.ReactNode;
props?: PropsWithChildren;
}
// DockIcon Component
const DockIcon = ({
size,
magnification = DEFAULT_MAGNIFICATION,
distance = DEFAULT_DISTANCE,
mouseX,
className,
children,
...props
}: DockIconProps) => {
const ref = useRef<HTMLDivElement>(null);
const distanceCalc = useTransform(mouseX, (val: number) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
let widthSync = useTransform(
distanceCalc,
[-distance, 0, distance],
[40, magnification, 40],
);
let width = useSpring(widthSync, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
return (
<motion.div
ref={ref}
style={{ width }}
className={cn(
"flex aspect-square cursor-pointer items-center justify-center rounded-full bg-neutral-400/40",
className,
)}
{...props}
>
{children}
</motion.div>
);
};
DockIcon.displayName = "DockIcon";
export { Dock, DockIcon, dockVariants };

Creating the Social Icons Component

Now, let's create our social media icons component. Create a new file components/social.tsx:


import { Dock, DockIcon } from '../components/dock';
import React from "react";
export type IconProps = React.HTMLAttributes<SVGElement>;
const Icons = {
gitHub: (props: IconProps) => (
<svg viewBox="0 0 438.549 438.549" {...props}>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
),
instagram: (props: IconProps) => (
<span className="[&>svg]:h-7 [&>svg]:w-7 [&>svg]:fill-[#c13584]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z" />
</svg>
</span>
),
facebook: (props: IconProps) => (
<span className="[&>svg]:h-7 [&>svg]:w-7 [&>svg]:fill-[#1877f2]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
<path d="M80 299.3V512H196V299.3h86.5l18-97.8H196V166.9c0-51.7 20.3-71.5 72.7-71.5c16.3 0 29.4 .4 37 1.2V7.9C291.4 4 256.4 0 236.2 0C129.3 0 80 50.5 80 159.4v42.1H14v97.8H80z" />
</svg>
</span>
),
linkedin: (props: IconProps) => (
<span className="[&>svg]:h-7 [&>svg]:w-7 [&>svg]:fill-[#0077b5]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M100.3 448H7.4V148.9h92.9zM53.8 108.1C24.1 108.1 0 83.5 0 53.8a53.8 53.8 0 0 1 107.6 0c0 29.7-24.1 54.3-53.8 54.3zM447.9 448h-92.7V302.4c0-34.7-.7-79.2-48.3-79.2-48.3 0-55.7 37.7-55.7 76.7V448h-92.8V148.9h89.1v40.8h1.3c12.4-23.5 42.7-48.3 87.9-48.3 94 0 111.3 61.9 111.3 142.3V448z" />
</svg>
</span>
),
tiktok: (props: IconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="30"
height="30"
viewBox="0 0 30 30"
>
<path d="M24,4H6C4.895,4,4,4.895,4,6v18c0,1.105,0.895,2,2,2h18c1.105,0,2-0.895,2-2V6C26,4.895,25.104,4,24,4z M22.689,13.474 c-0.13,0.012-0.261,0.02-0.393,0.02c-1.495,0-2.809-0.768-3.574-1.931c0,3.049,0,6.519,0,6.577c0,2.685-2.177,4.861-4.861,4.861 C11.177,23,9,20.823,9,18.139c0-2.685,2.177-4.861,4.861-4.861c0.102,0,0.201,0.009,0.3,0.015v2.396c-0.1-0.012-0.197-0.03-0.3-0.03 c-1.37,0-2.481,1.111-2.481,2.481s1.11,2.481,2.481,2.481c1.371,0,2.581-1.08,2.581-2.45c0-0.055,0.024-11.17,0.024-11.17h2.289 c0.215,2.047,1.868,3.663,3.934,3.811V13.474z"></path>
</svg>
)
};
export function Social() {
return (
<>
<Dock>
<DockIcon>
<Icons.instagram className="h-6 w-6" />
</DockIcon>
<DockIcon>
<Icons.facebook className="h-6 w-6" />
</DockIcon>
<DockIcon>
<Icons.gitHub className="h-6 w-6" />
</DockIcon>
<DockIcon>
<Icons.tiktok className="h-6 w-6" />
</DockIcon>
<DockIcon>
<Icons.linkedin className="h-6 w-6" />
</DockIcon>
</Dock>
</>
);
}

How It Works

  1. The Dock component acts as a container and tracks mouse movement
  2. Each DockIcon responds to the mouse position using Framer Motion
  3. The magnification effect is achieved through the useSpring hook
  4. Social icons are rendered within the dock with their respective colors and styles

Customization

You can customize the dock's behavior by adjusting these props:

  • magnification: Controls the maximum size of icons on hover (default: 60)
  • distance: Controls the range of the magnification effect (default: 140)
  • className: Add custom styles to the dock or icons

Usage

Import and use the Social component in your React application:


import { Social } from './components/social';
export default function App() {
return (
<div className="container mx-auto p-4">
<Social />
</div>
);
}