👁️ Interactive Eye Animation with Skills Hover Effect using HTML, CSS, JavaScript & GSAP

In this tutorial, we’ll build a creative and animated “eye component” that follows your cursor around the screen. As a bonus, we’ll place technology skill icons around the eye in a circular layout, and change the pupil color when a skill is hovered!

Perfect for portfolios or creative websites, this animation uses:

  • HTML & CSS for layout and design

  • JavaScript for interactivity

  • GSAP (GreenSock) for smooth animations

  • Font Awesome for skill icons

✅ Live Demo

HTML Structure

<div class="mainContainer">
        <div class="eye">
            <div class="pupil"></div>

            <!-- skills -->
            <div class="skills">
                <span class="skill" data-color="#e34c26"><i class="fa-brands fa-html5"></i></span>
                <span class="skill" data-color="#264de4"><i class="fa-brands fa-css3-alt"></i></span>
                <span class="skill" data-color="#f0db4f"> <i class="fa-brands fa-square-js"></i></span>
                <span class="skill" data-color="#F7BB07"> <i class="fa-brands fa-python"></i></span>
                <span class="skill" data-color="#5ED4F3"> <i class="fa-brands fa-react"></i></span>
                <span class="skill" data-color="#6FA560"> <i class="fa-brands fa-node-js"></i></span>
            </div>
        </div>
    </div>

  • .eye is the outer eye shape

  • .pupil is the movable part that will follow the cursor

  • .skills is a container for technology icons

  • Each .skill span includes a Font Awesome icon and a custom data-color

🎨 Styling the Eye and Skills

We use absolute positioning for the pupil and icons to center them and move freely inside the eye. The clamp() function ensures responsiveness across devices.

.mainContainer {
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 1rem;
        }

        .mainContainer .eye {
            width: clamp(150px, 8vw, 300px);
            aspect-ratio: 1;
            border-radius: 50%;
            box-shadow: 0px 0px 5px #ffffff3e;
            position: relative;
            isolation: isolate;
        }

        .mainContainer .eye .pupil {
            width: 40px;
            aspect-ratio: 1;
            background-color: #c9c9c9;
            box-shadow: 0px 0px 4px #c9c9c9;
            border-radius: 50%;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }

        /* css for skills */
        .mainContainer .eye .skills {
            width: clamp(300px, 22vw, 500px);
            aspect-ratio: 1;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: -1;
            /* background-color: red; */
        }

        .mainContainer .eye .skills .skill {
            position: absolute;
            transform: translate(-50%, -50%);
            font-size: clamp(2rem, 2.5vw, 3.2rem);
            text-align: center;
            font-weight: bold;
            text-transform: uppercase;
            white-space: nowrap;
            cursor: pointer;
            color: rgba(255, 255, 255, 0.419);
            padding: 2px 6px;
            border-radius: 4px;
        }

        /* we will set this --skill-color variable using js */
        .mainContainer .eye .skills .skill:hover {
            color: var(--skill-color);
            text-shadow: 0px 0px 5px var(--skill-color);
        }

🧠 JavaScript Logic Explained

🔸 1. Arrange Skills in a Circle

We evenly space the skills around the .skills container using cosine and sine math:

const skillsContainer = document.querySelector('.skills');
        const skills = skillsContainer.querySelectorAll(".skill");
        const total = skills.length;
        //get container radius based on size
        const rect = skillsContainer.getBoundingClientRect();
        const radius = rect.width/2 - 30; // 30 is used as offset to keep skills inside the div

        //arrange skills around the skills container
        skills.forEach((skill,i)=>{
            //divide circle into equal slices
            const angleInRadians = (i/total) * Math.PI *2;

            //get X and Y using circle formula
            const x = Math.cos(angleInRadians) * radius;
            const y = Math.sin(angleInRadians) * radius;

            //position skill element
            skill.style.left = `calc(50% + ${x}px)`;
            skill.style.top = `calc(50% + ${y}px)`;
        })

Then place each skill at left: 50% + x and top: 50% + y.

🔸 2. Make the Pupil Follow the Cursor

// move pupil along with the mouse
        document.addEventListener('mousemove', (e) => {
            // get the eye's position and size
            const eyeBoxRect = eye.getBoundingClientRect();

            //find the center of the eye
            const centerX = eyeBoxRect.left + eyeBoxRect.width / 2;
            const centerY = eyeBoxRect.top + eyeBoxRect.height / 2;

            //find how far mouse is from the center 
            const moveX = e.clientX - centerX;
            const moveY = e.clientY - centerY;

            //to limit the movement of the pupil we need to calculate some things

            //find distance from cursor to the center of the eye
            const distance = Math.hypot(moveX, moveY);
            // console.log(distance) // is return the distance from cursor(x,y) to center of the eye in a straight line

            //find how far the pupil is allowed to move 
            const maxMove = (eyeBoxRect.width / 2) - (pupil.offsetWidth / 2) - 5;
            // console.log(maxMove); // it gives us the maximum allowed movement for the pupil

            //scal factor (from 0 to 1)
            const scale = Math.min(1, maxMove / distance || 0);
            //if the mouse is far away, scale becomes 1 (pupil moves to max limit)
            //if mouse is close , scale becomes less than 1 or zero (pupil moves less)

            // apply scaled X and Y
            const finalX = moveX * scale;
            const finalY = moveY * scale;

            // animate pupil
            gsap.to(pupil, {
                x: finalX,
                y: finalY,
                duration: 0.2,
                ease: "power2.out"
            })
        })

We calculate how far the cursor is from the center of the eye:

// get the eye's position and size
            const eyeBoxRect = eye.getBoundingClientRect();

            //find the center of the eye
            const centerX = eyeBoxRect.left + eyeBoxRect.width / 2;
            const centerY = eyeBoxRect.top + eyeBoxRect.height / 2;

            //find how far mouse is from the center 
            const moveX = e.clientX - centerX;
            const moveY = e.clientY - centerY;

           //find distance from cursor to the center of the eye
            const distance = Math.hypot(moveX, moveY);

We also calculate the maximum distance the pupil is allowed to move:

//find how far the pupil is allowed to move 
      const maxMove = (eyeBoxRect.width / 2) - (pupil.offsetWidth / 2) - 5;
      // console.log(maxMove); // it gives us the maximum allowed movement for the pupil

Then we scale the movement using the ratio of allowed movement to actual distance, to prevent the pupil from going outside the eye:

//scal factor (from 0 to 1)
            const scale = Math.min(1, maxMove / distance || 0);
            //if the mouse is far away, scale becomes 1 (pupil moves to max limit)
            //if mouse is close , scale becomes less than 1 or zero (pupil moves less)

Finally, animate it smoothly with GSAP:

// apply scaled X and Y
            const finalX = moveX * scale;
            const finalY = moveY * scale;

            // animate pupil
            gsap.to(pupil, {
                x: finalX,
                y: finalY,
                duration: 0.2,
                ease: "power2.out"
            })

🔸 3. Hover Effect on Skills

When a user hovers on a skill icon, we:

Grab the color code from the icon and Animate the pupil and eye's background and shadow to match that color

//change pupil color on hover of skills
        skills.forEach((skill)=>{

            //on mouse enter
            skill.addEventListener('mouseenter',()=>{
                const color = skill.getAttribute('data-color');
                skill.style.setProperty('--skill-color',color);
                //for pupil
                gsap.to(pupil,{
                    backgroundColor:color,
                    boxShadow:`0px 0px 15px ${color}`,
                    duration:0.3
                });
                //for eye
                gsap.to(eye,{
                    boxShadow:`0px 0px 15px ${color}`,
                    duration:0.3
                });

            })

            // set color to default one on mouseleave
            skill.addEventListener('mouseleave',()=>{
                gsap.to(pupil,{
                     backgroundColor:"#c9c9c9",
                    boxShadow:`0px 0px 15px #c9c9c9`,
                    duration:0.3
                })
                 gsap.to(eye,{
                    boxShadow:`0px 0px 15px #ffffff3e`,
                    duration:0.3
                });
            })
        })

When they leave, we reset the colors back.

Tags : #eye follows cursor animation #cursor tracking effect HTML CSS JS #GSAP eye animation #hover effect on skill icons #circular layout skill icons #interactive web UI with JavaScript #creative portfolio animation #eye pupil follows mouse #JavaScript eye animation tutorial #HTML CSS JS GSAP hover animation

Recent blog Posts

search post

search video tutorial