Docs
Dock
Dock
An interactive dock component inspired by macOS dock with animation capabilities.
References
Inspiration
Framer Motion Courses (not affiliated)
Animation Teachers (also not affiliated)
Installation
Copy and paste the following code into your project.
"use client"
import React, {
ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import {
AnimatePresence,
MotionValue,
animate,
motion,
useAnimation,
useMotionValue,
useSpring,
useTransform,
} from "framer-motion"
import { cn } from "@/lib/utils"
// Interface to define the types for our Dock context
interface DockContextType {
width: number // Width of the dock
hovered: boolean // If the dock is hovered
setIsZooming: (value: boolean) => void // Function to set zooming state
zoomLevel: MotionValue<number> // Motion value for zoom level
mouseX: MotionValue<number> // Motion value for mouse X position
animatingIndexes: number[] // Array of animating indexes
setAnimatingIndexes: (indexes: number[]) => void // Function to set animating indexes
}
// Initial width for the dock
const INITIAL_WIDTH = 48
// Create a context to manage Dock state
const DockContext = createContext<DockContextType>({
width: 0,
hovered: false,
setIsZooming: () => {},
zoomLevel: null as any,
mouseX: null as any,
animatingIndexes: [],
setAnimatingIndexes: () => {},
})
// Custom hook to use Dock context
const useDock = () => useContext(DockContext)
// Props for the Dock component
interface DockProps {
className?: string
children: ReactNode // React children to be rendered within the dock
}
// Main Dock component: orchestrating the dock's animation behavior
function Dock({ className, children }: DockProps) {
const [hovered, setHovered] = useState(false) // State to track if the dock is hovered. When the mouse hovers over the dock, this state changes to true.
const [width, setWidth] = useState(0) // State to track the width of the dock. This dynamically updates based on the dock's current width.
const dockRef = useRef<HTMLDivElement>(null) // Reference to the dock element in the DOM. This allows direct manipulation and measurement of the dock.
const isZooming = useRef(false) // Reference to track if the zooming animation is active. This prevents conflicting animations.
const [animatingIndexes, setAnimatingIndexes] = useState<number[]>([]) // State to track which dock items are currently animating. This array holds the indices of animating items.
// Callback to toggle the zooming state. This ensures that we don't trigger hover animations while zooming.
const setIsZooming = useCallback((value: boolean) => {
isZooming.current = value // Update the zooming reference
setHovered(!value) // Update the hover state based on zooming
}, [])
const zoomLevel = useMotionValue(1) // Motion value for the zoom level of the dock. This provides a smooth zooming animation.
// Hook to handle window resize events and update the dock's width accordingly.
useWindowResize(() => {
setWidth(dockRef.current?.clientWidth || 0) // Set width to the dock's current width or 0 if undefined
})
const mouseX = useMotionValue(Infinity) // Motion value to track the mouse's X position relative to the viewport. Initialized to Infinity to denote no tracking initially.
return (
// Provide the dock's state and control methods to the rest of the application through context.
<DockContext.Provider
value={{
hovered, // Current hover state of the dock
setIsZooming, // Method to set the zooming state
width, // Current width of the dock
zoomLevel, // Current zoom level motion value
mouseX, // Current mouse X position motion value
animatingIndexes, // Current animating indexes
setAnimatingIndexes, // Method to set animating indexes
}}
>
<motion.div
ref={dockRef} // Reference to the dock element
// className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-end h-14 p-2 gap-3 bg-neutral-50 dark:bg-black bg-opacity-90 rounded-xl"
className={cn(
"absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-end h-14 p-2 gap-3 bg-opacity-90 rounded-xl",
" dark:bg-neutral-900 bg-neutral-50 p-2 no-underline shadow-sm transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800/80 ",
"shadow-[0px_1px_1px_0px_rgba(0,0,0,0.05),0px_1px_1px_0px_rgba(255,252,240,0.5)_inset,0px_0px_0px_1px_hsla(0,0%,100%,0.1)_inset,0px_0px_1px_0px_rgba(28,27,26,0.5)]",
"shadow-[rgba(17,24,28,0.08)_0_0_0_1px,rgba(17,24,28,0.08)_0_1px_2px_-1px,rgba(17,24,28,0.04)_0_2px_4px]",
"dark:shadow-[0_1px_0_0_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(0,0,0,0.1),0_2px_2px_0_rgba(0,0,0,0.1),0_4px_4px_0_rgba(0,0,0,0.1),0_8px_8px_0_rgba(0,0,0,0.1)]",
className
)}
// Event handler for mouse movement within the dock
onMouseMove={(e) => {
mouseX.set(e.pageX) // Update the mouseX motion value to the current mouse position
if (!isZooming.current) {
// Only set hovered if not zooming
setHovered(true) // Set hovered state to true
}
}}
// Event handler for when the mouse leaves the dock
onMouseLeave={() => {
mouseX.set(Infinity) // Reset mouseX motion value
setHovered(false) // Set hovered state to false
}}
style={{
x: "-50%", // Center the dock horizontally
scale: zoomLevel, // Bind the zoom level to the scale style property
}}
>
{children} {/* Render the dock's children within the motion div */}
</motion.div>
</DockContext.Provider>
)
}
// Props for the DockCardInner component
interface DockCardInnerProps {
src: string // Source URL for the image
id: string // Unique identifier for the card
children?: ReactNode // Optional children for the card
}
// DockCardInner component to display images and handle animation states
function DockCardInner({ src, id, children }: DockCardInnerProps) {
const { animatingIndexes } = useDock() // Access the Dock context to get the animating indexes. This determines which cards are currently animating.
return (
<span className="relative flex justify-center items-center z-0 overflow-hidden w-full h-full rounded-md">
{/* Background image with a blur effect to give a depth illusion */}
<motion.img
className="absolute z-10 opacity-40 filter blur-md transform translate-y-2.5 scale-125 "
src={src}
alt=""
/>
{/* AnimatePresence component to handle the entrance and exit animations of children - in our case, the "openIcon" */}
<AnimatePresence>
{animatingIndexes.includes(parseInt(id)) && children ? (
<motion.div
className="relative z-0 h-full w-full rounded-full"
initial={{ scale: 0, opacity: 0, filter: "blur(4px)" }}
animate={{
scale: 1,
opacity: 1,
filter: "blur(0px)",
transition: { type: "spring", delay: 0.2 }, // Animation to spring into place with a delay so our layoutId animations can be smooth
}}
exit={{
scale: 0,
opacity: 0,
filter: "blur(4px)",
transition: { duration: 0 }, // Exit animation with no delay
}}
>
<div className="h-full w-full flex flex-col items-center justify-center">
{/* Render the openIcon */}
{children}
</div>
</motion.div>
) : null}
</AnimatePresence>
{/* Another AnimatePresence to handle layout animations */}
<AnimatePresence mode="popLayout">
{!animatingIndexes.includes(parseInt(id)) ? (
<motion.img
layoutId={id} // Unique identifier for layout animations
className="relative z-0 w-1/2 h-1/2 rounded-full border border-black/30 dark:border-white/10"
src={src}
alt=""
/>
) : null}
</AnimatePresence>
</span>
)
}
// Props for the DockCard component
interface DockCardProps {
children: ReactNode // React children to be rendered within the dock card
id: string // Unique identifier for the dock card
}
// DockCard component: manages individual card behavior within the dock
function DockCard({ children, id }: DockCardProps) {
const cardRef = useRef<HTMLButtonElement>(null) // Reference to the card button element for direct DOM manipulation and measurement
const [elCenterX, setElCenterX] = useState(0) // State to store the center X position of the card for accurate mouse interaction calculations
const dock = useDock() // Access the Dock context to get shared state and control functions
// Spring animation for the size of the card, providing a smooth and responsive scaling effect
const size = useSpring(INITIAL_WIDTH, {
stiffness: 320,
damping: 20,
mass: 0.1,
})
// Spring animation for the opacity of the card, enabling smooth fade-in and fade-out effects
const opacity = useSpring(0, {
stiffness: 300,
damping: 20,
})
// Custom hook to track mouse position and update the card size dynamically based on proximity to the mouse
useMousePosition(
{
onChange: ({ value }) => {
const mouseX = value.x
if (dock.width > 0) {
// Calculate transformed value based on mouse position and card center, using a cosine function for a smooth curve
const transformedValue =
INITIAL_WIDTH +
36 *
Math.cos((((mouseX - elCenterX) / dock.width) * Math.PI) / 2) **
12
// Only animate size if the dock is hovered
if (dock.hovered) {
animate(size, transformedValue)
}
}
},
},
[elCenterX, dock]
)
// Hook to update the center X position of the card on window resize for accurate mouse interaction
useWindowResize(() => {
const { x } = cardRef.current?.getBoundingClientRect() || { x: 0 }
setElCenterX(x + 24) // 24 is the half of INITIAL_WIDTH (48 / 2), centering the element
})
const isAnimating = useRef(false) // Reference to track if the card is currently animating to avoid conflicting animations
const controls = useAnimation() // Animation controls for managing card's Y position during the animation loop
const timeoutRef = useRef<number | null>(null) // Reference to manage timeout cleanup on component unmount
// Handle click event to start or stop the card's animation
const handleClick = () => {
if (!isAnimating.current) {
isAnimating.current = true
// Add the card's id to the animatingIndexes array in the Dock context
dock.setAnimatingIndexes([...dock.animatingIndexes, parseInt(id)])
opacity.set(0.5) // Set opacity for the animation
controls.start({
y: -24, // Move the card up by 24 pixels
transition: {
repeat: Infinity, // Repeat the animation indefinitely
repeatType: "reverse", // Reverse the direction of the animation each cycle
duration: 0.5, // Duration of each cycle
},
})
} else {
isAnimating.current = false
// Remove the card's id from the animatingIndexes array in the Dock context
dock.setAnimatingIndexes(
dock.animatingIndexes.filter((index) => index !== parseInt(id))
)
opacity.set(0) // Reset opacity
controls.start({
y: 0, // Reset Y position to the original state
transition: { duration: 0.5 }, // Smooth transition back to original state
})
}
}
// Cleanup timeout on component unmount to prevent memory leaks
useEffect(() => {
return () => clearTimeout(timeoutRef.current!)
}, [])
// Calculate the distance from the mouse position to the center of the card
const distance = useTransform(dock.mouseX, (val) => {
const bounds = cardRef.current?.getBoundingClientRect() ?? {
x: 0,
width: 0,
}
return val - bounds.x - bounds.width / 2 // Calculate distance to the center of the card
})
// Transform the calculated distance into a responsive width for the card
let widthSync = useTransform(distance, [-150, 0, 150], [40, 80, 40])
let width = useSpring(widthSync, { mass: 0.1, stiffness: 150, damping: 12 })
return (
<div className="flex flex-col items-center gap-1" key={id}>
{/* Motion button for the card, handling click events and animations */}
<motion.button
ref={cardRef} // Reference to the button element
className="rounded-lg border aspect-square dark:border-white/5 border-black/5 border-opacity-10 dark:bg-neutral-800 bg-neutral-100 saturate-90 brightness-90 transition-filter duration-200 hover:saturate-100 hover:brightness-112"
onClick={handleClick} // Click handler to start/stop animation
style={{
width: width, // Responsive width based on mouse distance
}}
animate={controls} // Animation controls for Y position
whileTap={{ scale: 0.95 }} // Scale down slightly on tap for a tactile feel
>
{children}{" "}
{/* Render the children of the DockCard inside the motion button */}
</motion.button>
{/* AnimatePresence to manage the presence and layout animations of the card's indicator */}
<AnimatePresence mode="popLayout">
{dock.animatingIndexes.includes(parseInt(id)) ? (
<motion.div
key={id} // Unique key for the motion div
layoutId={id} // Layout identifier for smooth layout animations
className="rounded-full"
style={{ opacity }} // Bind opacity to the animated opacity spring
>
<motion.div
exit={{ transition: { duration: 0 } }} // Exit transition with no duration for immediate removal
className="w-1.5 h-1.5 rounded-full dark:bg-white bg-black"
style={{ opacity }} // Bind opacity to the animated opacity spring
/>
</motion.div>
) : null}
</AnimatePresence>
</div>
)
}
// Divider component for the dock
function DockDivider() {
return (
<motion.div
className="h-full flex items-center p-1.5 cursor-ns-resize"
drag="y"
dragConstraints={{ top: -100, bottom: 50 }}
>
<span className="w-0.5 h-full rounded dark:bg-neutral-100/10 bg-neutral-800/10 "></span>
</motion.div>
)
}
type UseWindowResizeCallback = (width: number, height: number) => void
// Custom hook to handle window resize events and invoke a callback with the new dimensions
function useWindowResize(callback: UseWindowResizeCallback) {
// Create a stable callback reference to ensure the latest callback is always used
const callbackRef = useCallbackRef(callback)
useEffect(() => {
// Function to handle window resize and call the provided callback with updated dimensions
const handleResize = () => {
callbackRef(window.innerWidth, window.innerHeight)
}
// Initial call to handleResize to capture the current window size
handleResize()
// Adding event listener for window resize events
window.addEventListener("resize", handleResize)
// Cleanup function to remove the event listener when the component unmounts or dependencies change
return () => {
window.removeEventListener("resize", handleResize)
}
}, [callbackRef]) // Dependency array includes the stable callback reference
}
// Custom hook to create a stable callback reference
function useCallbackRef<T extends (...args: any[]) => any>(callback: T): T {
// Use a ref to store the callback
const callbackRef = useRef(callback)
// Update the ref with the latest callback whenever it changes
useEffect(() => {
callbackRef.current = callback
})
// Return a memoized version of the callback that always uses the latest callback stored in the ref
return useMemo(() => ((...args) => callbackRef.current?.(...args)) as T, [])
}
// Interface for mouse position options
interface MousePositionOptions {
onChange?: (position: { value: { x: number; y: number } }) => void
}
// Custom hook to track mouse position and animate values accordingly
export function useMousePosition(
options: MousePositionOptions = {}, // Options to customize behavior, including an onChange callback
deps: readonly any[] = [] // Dependencies array to determine when the effect should re-run
) {
const { onChange } = options // Destructure onChange from options for use in the effect
// Create motion values for x and y coordinates, initialized to 0
const x = useMotionValue(0)
const y = useMotionValue(0)
useEffect(() => {
// Function to handle mouse move events, animating the x and y motion values to the current mouse coordinates
const handleMouseMove = (event: MouseEvent) => {
animate(x, event.clientX)
animate(y, event.clientY)
}
// Function to handle changes in the motion values, calling the onChange callback if it exists
const handleChange = () => {
if (onChange) {
onChange({ value: { x: x.get(), y: y.get() } })
}
}
// Subscribe to changes in the x and y motion values
const unsubscribeX = x.on("change", handleChange)
const unsubscribeY = y.on("change", handleChange)
// Add event listener for mouse move events
window.addEventListener("mousemove", handleMouseMove)
// Cleanup function to remove event listener and unsubscribe from motion value changes
return () => {
window.removeEventListener("mousemove", handleMouseMove)
unsubscribeX()
unsubscribeY()
}
}, [x, y, onChange, ...deps]) // Dependency array includes x, y, onChange, and any additional dependencies
// Memoize and return the motion values for x and y coordinates
return useMemo(
() => ({
x, // Motion value for x coordinate
y, // Motion value for y coordinate
}),
[x, y] // Dependencies for the memoized return value
)
}
export { Dock, DockCard, DockCardInner, DockDivider, useDock }
export Dock
Update the import paths to match your project setup.
Usage
import {
Card,
Dock,
DockCard,
DockDivider,
useDock,
} from "@/components/Dock/Dock"
// Example usage of the Dock component with animated cards and dividers
const DockDemo = () => {
const gradients = [
"https://products.ls.graphics/mesh-gradients/images/03.-Snowy-Mint_1-p-130x130q80.jpeg",
"https://products.ls.graphics/mesh-gradients/images/04.-Hopbush_1-p-130x130q80.jpeg",
"https://products.ls.graphics/mesh-gradients/images/06.-Wisteria-p-130x130q80.jpeg",
"https://products.ls.graphics/mesh-gradients/images/09.-Light-Sky-Blue-p-130x130q80.jpeg",
"https://products.ls.graphics/mesh-gradients/images/12.-Tumbleweed-p-130x130q80.jpeg",
"https://products.ls.graphics/mesh-gradients/images/15.-Perfume_1-p-130x130q80.jpeg",
null,
"https://products.ls.graphics/mesh-gradients/images/36.-Pale-Chestnut-p-130x130q80.jpeg",
]
const openIcons = [
<CircleIcon className="h-8 w-8 fill-black stroke-black rounded-full" />,
<TriangleIcon className="h-8 w-8 fill-black stroke-black rounded-full" />,
<SquareIcon className="h-8 w-8 fill-black stroke-black rounded-full" />,
<PentagonIcon className="h-8 w-8 fill-black stroke-black rounded-full" />,
<HexagonIcon className="h-8 w-8 fill-black stroke-black rounded-full" />,
<OctagonIcon className="h-8 w-8 fill-black stroke-black rounded-full" />,
<OctagonIcon className="h-8 w-8 fill-black stroke-black rounded-full" />, // skip
<BlocksIcon className="h-8 w-8 fill-black stroke-black rounded-full" />,
]
return (
<div className="min-h-screen flex items-center justify-center">
<Dock>
{gradients.map((src, index) =>
src ? (
<DockCard key={src} id={`${index}`}>
<Card src={src} id={`${index}`}>
{openIcons[index]}
</Card>
</DockCard>
) : (
<DockDivider key={index} />
)
)}
</Dock>
</div>
)
}
export default DockDemo
Dock Component
// Main Dock component: orchestrating the dock's animation behavior
const Dock = ({ children }: DockProps) => {
// State to track if the dock is hovered. When the mouse hovers over the dock, this state changes to true.
const [hovered, setHovered] = useState(false)
// State to track the width of the dock. This dynamically updates based on the dock's current width.
const [width, setWidth] = useState(0)
// Reference to the dock element in the DOM. This allows direct manipulation and measurement of the dock.
const dockRef = useRef<HTMLDivElement>(null)
// Reference to track if the zooming animation is active. This prevents conflicting animations.
const isZooming = useRef(false)
// State to track which dock items are currently animating. This array holds the indices of animating items.
const [animatingIndexes, setAnimatingIndexes] = useState<number[]>([])
// Callback to toggle the zooming state. This ensures that we don't trigger hover animations while zooming.
const setIsZooming = useCallback((value: boolean) => {
isZooming.current = value // Update the zooming reference
setHovered(!value) // Update the hover state based on zooming
}, [])
// Motion value for the zoom level of the dock. This provides a smooth zooming animation.
const zoomLevel = useMotionValue(1)
// Hook to handle window resize events and update the dock's width accordingly.
useWindowResize(() => {
setWidth(dockRef.current?.clientWidth || 0) // Set width to the dock's current width or 0 if undefined
})
// Motion value to track the mouse's X position relative to the viewport. Initialized to Infinity to denote no tracking initially.
const mouseX = useMotionValue(Infinity)
return (
// Provide the dock's state and control methods to the rest of the application through context.
<DockContext.Provider
value={{
hovered, // Current hover state of the dock
setIsZooming, // Method to set the zooming state
width, // Current width of the dock
zoomLevel, // Current zoom level motion value
mouseX, // Current mouse X position motion value
animatingIndexes, // Current animating indexes
setAnimatingIndexes, // Method to set animating indexes
}}
>
<motion.div
ref={dockRef} // Reference to the dock element
className="fixed bottom-3 left-1/2 transform -translate-x-1/2 flex items-end h-14 p-2 gap-3 bg-black bg-opacity-90 rounded-xl"
// Event handler for mouse movement within the dock
onMouseMove={(e) => {
mouseX.set(e.pageX) // Update the mouseX motion value to the current mouse position
if (!isZooming.current) {
// Only set hovered if not zooming
setHovered(true) // Set hovered state to true
}
}}
// Event handler for when the mouse leaves the dock
onMouseLeave={() => {
mouseX.set(Infinity) // Reset mouseX motion value
setHovered(false) // Set hovered state to false
}}
style={{
x: "-50%", // Center the dock horizontally
scale: zoomLevel, // Bind the zoom level to the scale style property
}}
>
{children} {/* Render the dock's children within the motion div */}
</motion.div>
</DockContext.Provider>
)
}