Eye-Tracking UI: Designing Gaze-Driven Interfaces That Work
Source: Dev.to
Create an accessible, testable eye‑tracking UI page with HTML, CSS, and JavaScript. Start with a simple HTML structure, then progressively layer on CSS for layout and styling and JavaScript for gaze‑driven interactions. Below is a concise, step‑by‑step guide that makes the process easy to follow.
Demo:
Source:
What you’ll build (overview)
- A single page with a card‑like container and one or more “eyes” (visual elements that move their pupils toward the user’s gaze or mouse pointer).
- Gaze‑driven interactions: pupils follow the user’s gaze or cursor; dwell detection can trigger actions; graceful fallback when no eye‑tracker is available.
HTML – basic, semantic structure
Create a minimal skeleton so styling and behavior can be added incrementally. Keep markup simple and accessible (ARIA labels where appropriate).
Eye‑Tracking UI Demo
## Cookie Monster
Watch the eyes follow you!
Key elements
.stage– outer container for the whole demo..cards– flex wrapper that centers the card vertically and horizontally..card– the visual panel that holds the eyes and any text..eye– the white part of an eye (.leyeand.reyefor left/right)..pupil– the dark circle that moves inside the eye.
CSS – layout and visual design (progressive enhancements)
Start with layout, then style the card, eyes, and pupils. Use CSS variables for easy theming and responsive rules for different screen sizes.
/* style.css */
:root {
--eye-color: #000000;
--card-eye-color: #ffffff;
--background-color: #2f86f1;
--font-family: 'Arial, sans-serif';
}
/* Global reset */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Page basics */
html, body {
height: 100%;
width: 100%;
font-family: var(--font-family);
background-color: var(--background-color);
}
/* Center the stage */
.cards {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
/* Card styling */
.card {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
width: 300px;
height: 400px;
padding: 20px;
text-align: center;
}
/* Blue area that holds the eyes */
.blue-area {
background-color: var(--background-color);
height: 250px;
width: 100%;
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
}
/* Eye shape */
.eye {
width: 100px;
height: 100px;
background-color: var(--card-eye-color);
border-radius: 50%;
margin: 0 10px;
position: relative;
overflow: hidden;
}
/* Pupil */
.pupil {
width: 50px;
height: 50px;
background-color: var(--eye-color);
border-radius: 50%;
position: absolute;
top: 20px;
left: 20px;
transition: transform 0.1s ease-out;
}
/* Typography */
.card h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.card p {
font-size: 1rem;
font-weight: 500;
color: #000;
}
/* Responsive tweaks */
@media (max-width: 500px) {
.card {
width: 90%;
height: auto;
}
.eye {
width: 80px;
height: 80px;
}
.pupil {
width: 40px;
height: 40px;
top: 20px;
left: 20px;
}
}
JavaScript – make it gaze‑driven (progressive approach)
Start with a mouse‑driven prototype. The script listens for mousemove on the stage, computes the vector from each eye’s center to the pointer, clamps the pupil movement within the eye’s bounds, and updates the pupil using CSS transform. requestAnimationFrame ensures smooth updates.
// script.js
const stage = document.querySelector('.stage');
const eyes = document.querySelectorAll('.eye');
function movePupil(event) {
eyes.forEach(eye => {
const pupil = eye.querySelector('.pupil');
const rect = eye.getBoundingClientRect();
// Eye center coordinates
const eyeX = rect.left + rect.width / 2;
const eyeY = rect.top + rect.height / 2;
// Vector from eye center to pointer
const dx = event.clientX - eyeX;
const dy = event.clientY - eyeY;
// Distance and angle
const angle = Math.atan2(dy, dx);
const distance = Math.min(20, Math.hypot(dx, dy)); // limit movement
// New pupil position (relative to eye)
const offsetX = Math.cos(angle) * distance;
const offsetY = Math.sin(angle) * distance;
// Apply transform
pupil.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
});
}
// Use requestAnimationFrame for throttling
let pending = false;
stage.addEventListener('mousemove', e => {
if (!pending) {
pending = true;
requestAnimationFrame(() => {
movePupil(e);
pending = false;
});
}
});
Adding real eye‑tracking (optional)
If a webcam‑based eye‑tracker is available (e.g., via the WebGazer library), replace the mouse coordinates with the gaze coordinates returned by the tracker. Keep the same movePupil logic; just feed it the gaze point instead of event.clientX/Y.
Testing and iteration
- Test on various screen sizes and lighting conditions.
- Verify that keyboard navigation and mouse‑only interaction work correctly.
- Measure latency, jitter, and false‑dwell triggers; adjust sensitivity constants (e.g., the
distanceclamp) as needed. - If you log interaction metrics, obtain clear user consent and anonymize data.
Enhancements and polish
- Add subtle hover or dwell animations (e.g., a ripple effect around the eye).
- Provide UI controls for sensitivity and dwell‑time thresholds.
- Use visual affordances (outlines, glow) to indicate when gaze focus is active.
- Optimize performance: limit DOM reads/writes, use only CSS transforms, and avoid heavy per‑frame calculations.
Example structure summary (quick reference)
- HTML:
.cards > .card > .eye > .pupil - CSS: variables for colors/sizes,
transitionfor smooth pupil movement, responsive media queries. - JS: modular input handler (mouse fallback + optional gaze adapter) that updates pupil positions via
transform.
Final notes
- Keep interactions simple and predictable; excessive autonomous movement can confuse users.
- Prioritize privacy—clearly explain any camera or sensor usage and obtain consent.
- Build incrementally: validate the experience with mouse input first, then integrate real gaze data and fine‑tune thresholds.