Eye Widget
An interactive eye that follows your mouse cursor.
Code
tsx
import { signal, onMount, createRef } from 'kaori.js';
function EyeWidget() {
const pupilX = signal(0);
const pupilY = signal(0);
const eyeContainerRef = createRef<HTMLDivElement>();
onMount(() => {
const eyeElement = eyeContainerRef.value;
if (!eyeElement) return;
const IRIS_DIAMETER = 112;
const PUPIL_DIAMETER = 48;
const irisRadius = IRIS_DIAMETER / 2;
const pupilRadius = PUPIL_DIAMETER / 2;
const maxPupilMovement = irisRadius - pupilRadius;
function handleMouseMove(event: MouseEvent) {
const eyeRect = eyeElement.getBoundingClientRect();
const eyeCenterX = eyeRect.left + eyeRect.width / 2;
const eyeCenterY = eyeRect.top + eyeRect.height / 2;
const deltaX = event.clientX - eyeCenterX;
const deltaY = event.clientY - eyeCenterY;
const angle = Math.atan2(deltaY, deltaX);
const distance = Math.min(
Math.sqrt(deltaX * deltaX + deltaY * deltaY),
maxPupilMovement
);
pupilX.value = Math.cos(angle) * distance;
pupilY.value = Math.sin(angle) * distance;
}
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
});
return () => (
<div class="eye-container">
<div ref={eyeContainerRef} class="eye">
<div class="iris">
<div
class="pupil"
style={{
transform: `translate(${pupilX.value}px, ${pupilY.value}px)`,
}}
/>
</div>
<div class="glare" />
</div>
</div>
);
}
render(<EyeWidget />, document.getElementById('root')!);Styling
css
.eye-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
}
.eye {
position: relative;
width: 160px;
height: 160px;
background: white;
border-radius: 50%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
transition: transform 0.2s ease-out;
}
.eye:hover {
transform: scale(1.05);
}
.iris {
width: 112px;
height: 112px;
background: linear-gradient(135deg, #4f46e5 0%, #2563eb 100%);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: inset 0 4px 8px rgba(0, 0, 0, 0.2);
}
.pupil {
position: absolute;
width: 48px;
height: 48px;
background: #1f2937;
border-radius: 50%;
transition: transform 0.1s ease-out;
}
.glare {
position: absolute;
width: 32px;
height: 32px;
background: white;
border-radius: 50%;
opacity: 0.7;
top: 25%;
left: 25%;
transform: translate(-50%, -50%);
pointer-events: none;
}How It Works
- Refs - We use
createRefto access the eye DOM element - Mouse Tracking - Event listener tracks mouse position
- Math - Calculate angle and distance from eye center to cursor
- Constraints - Clamp pupil movement to stay within iris
- Signals - pupilX and pupilY signals trigger re-renders
- Transform - CSS transform moves the pupil smoothly
Enhancements
Add blinking:
tsx
function EyeWidget() {
// ... existing code ...
const isBlinking = signal(false);
onMount(() => {
const blinkInterval = setInterval(() => {
isBlinking.value = true;
setTimeout(() => (isBlinking.value = false), 150);
}, 3000);
return () => clearInterval(blinkInterval);
});
return () => (
<div class="eye" classMap={{ blinking: isBlinking.value }}>
{/* ... */}
</div>
);
}css
.eye.blinking {
animation: blink 0.15s ease-in-out;
}
@keyframes blink {
0%,
100% {
transform: scaleY(1);
}
50% {
transform: scaleY(0.1);
}
}