In dit artikel legt Abhishek Jakhar uit hoe hij de gebruikerservaring van tooltips heeft verbeterd door een specifiek interactiepatroon te implementeren. Het probleem was dat tooltips zonder vertraging te veel visuele ruis veroorzaakten, terwijl een vaste vertraging het navigeren tussen meerdere elementen traag maakte.
De oplossing is het 'warm window' patroon:
- Koude start: De eerste tooltip opent na een vertraging van 200ms om onbedoelde triggers te voorkomen.
- Warm window: Zodra een tooltip is geopend, wordt de pagina 'warm'. Bij het verlaten van een element start een cooldown-timer van 300ms.
- Directe toegang: Indien er binnen deze cooldown-periode over een ander element wordt gehoverd, opent de tooltip direct zonder vertraging of animatie.
- Terug naar koud: Na het verlopen van de timer wordt de pagina weer 'koud' en is de initiële vertraging opnieuw van kracht.
Technisch is dit gerealiseerd in React met behulp van een TooltipProvider, useRef voor performante statusupdates en CSS-data-attributen om animaties over te slaan. De auteur concludeert dat dit soort details het verschil maken tussen een simpelweg 'werkend' product en een 'gepolijst' product, waarbij menselijk oordeel en smaak essentieel zijn om AI effectief aan te sturen.
Tooltips hebben vertraging nodig, en daarna moeten ze die overslaan
Op de pagina's met vragen voor mijn product, FrontPrep, toon ik bedrijfslogo's naast de interviewvragen. Wanneer je met de muis over een logo beweegt (hover), verschijnt er een tooltip met de naam van het bedrijf.
Een bepaald aspect irriteerde me al enkele dagen. Als ik de cursor simpelweg over de pagina bewoog, verschenen er constant tooltips langs de route. De oorzaak was simpel: ik had de vertraging van de tooltip op 0 gezet, waardoor deze direct verscheen zodra de cursor over het logo bewoog.
Om dit op te lossen, voegde ik een transitievertraging van 200ms toe. Dit werkte, maar creëerde een nieuw probleem. In de gebruikersinterface zijn er rijen waar meerdere logo's naast elkaar staan, omdat dezelfde interviewvragen bij meerdere bedrijven worden gesteld. Nu betekende het overstappen van het ene naar het andere logo dat je telkens opnieuw 200ms moest wachten, wat traag aanvoelde en leidde tot een slechte gebruikerservaring.
Dit bericht gaat niet over het bouwen van een tooltip. Het gaat over één klein interactiepatroon, hetzelfde patroon dat je vindt in browser-toolbars en op diverse websites, maar waar veel mensen zich niet bewust van zijn.
Een oplossing
Ik verdeel mijn oplossing in drie delen:
- Je beweegt de muis over een logo. De tooltip wacht 200ms voordat hij opent.
- Een tooltip sluit, waarna een timer van 300ms start. Ik noem dit het warm window of de warm page. Als je over een ander logo beweegt terwijl deze timer van 300ms loopt (wanneer de pagina "warm" is), opent de tooltip direct, zonder wachttijd of animatie.
- Wanneer de timer van 300ms verloopt, keert alles terug naar normaal en wordt de pagina "koud". Als je vervolgens weer over een logo beweegt, moet de volgende tooltip opnieuw 200ms wachten. Zonder deze stap zou de eerste geopende tooltip de vertraging voor de hele pagina permanent uitschakelen.
Het proces in het kort: hover → wacht 200ms → tooltip opent (pagina is nu warm) leave → tooltip sluit → 300ms cooldown → hover over een andere tooltip vóór de cooldown → opent direct, pagina blijft warm → cooldown eindigt → pagina is koud, de wachttijd van 200ms is terug
In FrontPrep zijn mijn tooltips gebouwd met Radix en Motion. Ik demonstreer het patroon hieronder met een eenvoudigere React-versie.
Hoe de code werkt
Ik leg de code uit in de volgorde waarin de gebeurtenissen plaatsvinden wanneer je de cursor beweegt.
Stap 1: Je beweegt over een logo
Wanneer onMouseEnter wordt geactiveerd op een logo, wordt deze functie aangeroepen:
function handleEnter() {
// `tooltips` is een useContext variabele waarin de isWarm status wordt bijgehouden tussen componenten
if (tooltips.isWarm) {
show();
return;
}
// `openTimer` is een useRef variabele, dus .current is hoe je de waarde instelt/benadert.
openTimer.current = setTimeout(show, tooltips.openDelay);
}
Bij het hoveren stelt dit component één vraag: is de pagina warm? Zo ja, dan opent de tooltip direct. Zo nee, dan start er een timer van 200ms. Het tooltips object komt voort uit de TooltipProvider.
Stap 2: De tooltip opent
Hier is de show functie die het openen van de tooltip afhandelt:
function show() {
setInstant(tooltips.isWarm);
setOpen(true);
tooltips.markOpened();
}
Deze functie doet drie dingen:
- Hij kopieert de waarde van
tooltips.isWarm naar een state genaamd instant.
- Hij opent de tooltip.
- Hij meldt aan de provider dat de tooltip open is, waardoor de pagina "warm" wordt.
De instant vlag wordt als data-attribuut aan de tooltip toegevoegd, wat CSS gebruikt om de beginanimatie over te slaan:
.tooltip[data-instant="true"] {
transition-duration: 0ms;
}
Dit is de reden waarom een tooltip direct zonder animatie of vertraging opent wanneer de pagina warm is.
Stap 3: Je verlaat het logo
Wanneer onMouseLeave wordt geactiveerd op een logo, wordt deze functie aangeroepen:
function handleLeave() {
clearTimeout(openTimer.current);
if (!open) return;
setOpen(false);
setInstant(false);
tooltips.markClosed();
}
Eerst moet worden vastgesteld of de tooltip momenteel open is. Stel dat je cursor in 50ms over een logo beweegt (veel minder dan 200ms); de timer gestart door handleEnter loopt dan nog en de tooltip is nog niet geopend. clearTimeout in handleLeave annuleert die timer, zodat de tooltip helemaal niet opent.
Als de tooltip wel open is (omdat de cursor langer dan 200ms bleef rusten of de pagina warm was), sluiten we de tooltip en informeren we de provider. De provider start vervolgens een cooldown-timer van 300ms. Als je vóór het einde van deze timer over het volgende logo beweegt, opent de tooltip direct.
Stap 4: De Provider
const warm = useRef(false);
const cooldownTimer = useRef(null);
useEffect(() => () => clearTimeout(cooldownTimer.current), []);
const tooltips = useMemo(
() => ({
openDelay,
isWarm: () => skipWhenWarm && warm.current,
markOpened() {
warm.current = true;
clearTimeout(cooldownTimer.current);
},
markClosed() {
clearTimeout(cooldownTimer.current);
cooldownTimer.current = setTimeout(() => {
warm.current = false;
}, warmFor);
},
}),
[openDelay, warmFor, skipWhenWarm]
);
Dit is de gedeelde status die nodig is om dit patroon te laten werken. De pagina is ofwel warm of koud. markOpened annuleert lopende cooldowns; dit zorgt ervoor dat de pagina warm blijft wanneer je van het ene naar het andere logo beweegt. In feite annuleert elke nieuwe tooltip de cooldown die door de vorige is gestart.
Een belangrijk detail is dat isWarm een ref is en geen React-state, omdat het wijzigen ervan niet alle tooltips op de pagina opnieuw hoeft te renderen. Deze ref wordt alleen uitgelezen binnen de event handlers.
Waarom een timer van 200ms?
Als we een timer van minder dan 150ms kiezen, zal de tooltip waarschijnlijk openen wanneer een cursor er simpelweg overheen beweegt. Kiezen we meer dan 250ms, dan voelt de hover traag en defect aan. 200ms is in dit geval een goede, gebalanceerde waarde.
Oordeel en smaak
Als je AI vraagt om een tooltip te bouwen, zal het een volledig functionele tooltip maken. Maar uiteindelijk ben jij als mens degene die beslist of de gebouwde tooltip van voldoende kwaliteit is. Er zijn details die een "werkende" tooltip onderscheiden van een "gepolijste" tooltip, op dezelfde manier als dat een werkend product onderscheidt van een gepolijst product.
AI kan alleen een gepolijst product bouwen als je het daartoe stuurt. Dat kun je doen wanneer je smaak en oordeel hebt ontwikkeld, wat voortkomt uit jaren van ervaring, fouten en oefening. Je kunt wel gebruikmaken van de expertise van andere engineers of designers om hun standaard van een gepolijst product over te nemen, maar je bent uiteindelijk nog steeds degene die beoordeelt en beslist of het aan de standaarden voldoet om te worden gepubliceerd.
Skill: Tooltip Timing
Ik heb een Claude-skill gemaakt om het bovenstaande probleem op te lossen. Je kunt deze gebruiken om je eigen codebase te auditeren. Omdat elke codebase anders is en verschillende libraries gebruikt, is deze skill generiek geschreven.
Naam: tooltip Beschrijving: Tooltips hebben vertraging nodig zodat ze niet openen bij onbedoelde muisbewegingen.
Theorie
Een klik is altijd intentioneel, terwijl een hover dat niet is. De cursor beweegt over de pagina om ergens te komen en passeert onderweg elementen; een tooltip kan aan de hand van de hover alleen niet weten of de gebruiker hem echt wil openen. De 200ms is de manier om dit te bepalen.
.tooltip {
transition-delay: 200ms;
}
De drie waarden en statussen
| Waarde | Getal | Waarom |
| Open delay | 200ms | Onder de 150ms opent een cursor die alleen passeert de trigger al. Boven de 250ms voelt een bewuste hover traag aan. |
| Warm window | 300ms | Lang genoeg om de beweging van de ene trigger naar de volgende te dekken. Kort genoeg zodat een hover een seconde later opnieuw moet wachten. |
| Close delay | 0ms | Het verlaten van een trigger moet duidelijk zijn. Er is dus geen reden om te wachten. |
Gewenst gedrag
hover → wacht 200ms → tooltip opent (pagina is nu warm) leave → tooltip sluit → 300ms cooldown
Tooltips hebben vertraging nodig, en daarna moeten ze die overslaan
Op de pagina's met vragen voor mijn product, FrontPrep, toon ik bedrijfslogo's naast de interviewvragen. Wanneer je met de muis over een logo beweegt (hover), verschijnt er een tooltip met de naam van het bedrijf.
Een bepaald aspect irriteerde me al enkele dagen. Als ik de cursor simpelweg over de pagina bewoog, verschenen er constant tooltips langs de route. De oorzaak was simpel: ik had de vertraging van de tooltip op 0 gezet, waardoor deze direct verscheen zodra de cursor over het logo bewoog.
Om dit op te lossen, voegde ik een transitievertraging van 200ms toe. Dit werkte, maar creëerde een nieuw probleem. In de gebruikersinterface zijn er rijen waar meerdere logo's naast elkaar staan, omdat dezelfde interviewvragen bij meerdere bedrijven worden gesteld. Nu betekende het overstappen van het ene naar het andere logo dat je telkens opnieuw 200ms moest wachten, wat traag aanvoelde en leidde tot een slechte gebruikerservaring.
Dit bericht gaat niet over het bouwen van een tooltip. Het gaat over één klein interactiepatroon, hetzelfde patroon dat je vindt in browser-toolbars en op diverse websites, maar waar veel mensen zich niet bewust van zijn.
Een oplossing
Ik verdeel mijn oplossing in drie delen:
- Je beweegt de muis over een logo. De tooltip wacht 200ms voordat hij opent.
- Een tooltip sluit, waarna een timer van 300ms start. Ik noem dit het warm window of de warm page. Als je over een ander logo beweegt terwijl deze timer van 300ms loopt (wanneer de pagina "warm" is), opent de tooltip direct, zonder wachttijd of animatie.
- Wanneer de timer van 300ms verloopt, keert alles terug naar normaal en wordt de pagina "koud". Als je vervolgens weer over een logo beweegt, moet de volgende tooltip opnieuw 200ms wachten. Zonder deze stap zou de eerste geopende tooltip de vertraging voor de hele pagina permanent uitschakelen.
Het proces in het kort: hover → wacht 200ms → tooltip opent (pagina is nu warm) leave → tooltip sluit → 300ms cooldown → hover over een andere tooltip vóór de cooldown → opent direct, pagina blijft warm → cooldown eindigt → pagina is koud, de wachttijd van 200ms is terug
In FrontPrep zijn mijn tooltips gebouwd met Radix en Motion. Ik demonstreer het patroon hieronder met een eenvoudigere React-versie.
Hoe de code werkt
Ik leg de code uit in de volgorde waarin de gebeurtenissen plaatsvinden wanneer je de cursor beweegt.
Stap 1: Je beweegt over een logo
Wanneer onMouseEnter wordt geactiveerd op een logo, wordt deze functie aangeroepen:
function handleEnter() {
// `tooltips` is een useContext variabele waarin de isWarm status wordt bijgehouden tussen componenten
if (tooltips.isWarm) {
show();
return;
}
// `openTimer` is een useRef variabele, dus .current is hoe je de waarde instelt/benadert.
openTimer.current = setTimeout(show, tooltips.openDelay);
}
Bij het hoveren stelt dit component één vraag: is de pagina warm? Zo ja, dan opent de tooltip direct. Zo nee, dan start er een timer van 200ms. Het tooltips object komt voort uit de TooltipProvider.
Stap 2: De tooltip opent
Hier is de show functie die het openen van de tooltip afhandelt:
function show() {
setInstant(tooltips.isWarm);
setOpen(true);
tooltips.markOpened();
}
Deze functie doet drie dingen:
- Hij kopieert de waarde van
tooltips.isWarm naar een state genaamd instant.
- Hij opent de tooltip.
- Hij meldt aan de provider dat de tooltip open is, waardoor de pagina "warm" wordt.
De instant vlag wordt als data-attribuut aan de tooltip toegevoegd, wat CSS gebruikt om de beginanimatie over te slaan:
.tooltip[data-instant="true"] {
transition-duration: 0ms;
}
Dit is de reden waarom een tooltip direct zonder animatie of vertraging opent wanneer de pagina warm is.
Stap 3: Je verlaat het logo
Wanneer onMouseLeave wordt geactiveerd op een logo, wordt deze functie aangeroepen:
function handleLeave() {
clearTimeout(openTimer.current);
if (!open) return;
setOpen(false);
setInstant(false);
tooltips.markClosed();
}
Eerst moet worden vastgesteld of de tooltip momenteel open is. Stel dat je cursor in 50ms over een logo beweegt (veel minder dan 200ms); de timer gestart door handleEnter loopt dan nog en de tooltip is nog niet geopend. clearTimeout in handleLeave annuleert die timer, zodat de tooltip helemaal niet opent.
Als de tooltip wel open is (omdat de cursor langer dan 200ms bleef rusten of de pagina warm was), sluiten we de tooltip en informeren we de provider. De provider start vervolgens een cooldown-timer van 300ms. Als je vóór het einde van deze timer over het volgende logo beweegt, opent de tooltip direct.
Stap 4: De Provider
const warm = useRef(false);
const cooldownTimer = useRef(null);
useEffect(() => () => clearTimeout(cooldownTimer.current), []);
const tooltips = useMemo(
() => ({
openDelay,
isWarm: () => skipWhenWarm && warm.current,
markOpened() {
warm.current = true;
clearTimeout(cooldownTimer.current);
},
markClosed() {
clearTimeout(cooldownTimer.current);
cooldownTimer.current = setTimeout(() => {
warm.current = false;
}, warmFor);
},
}),
[openDelay, warmFor, skipWhenWarm]
);
Dit is de gedeelde status die nodig is om dit patroon te laten werken. De pagina is ofwel warm of koud. markOpened annuleert lopende cooldowns; dit zorgt ervoor dat de pagina warm blijft wanneer je van het ene naar het andere logo beweegt. In feite annuleert elke nieuwe tooltip de cooldown die door de vorige is gestart.
Een belangrijk detail is dat isWarm een ref is en geen React-state, omdat het wijzigen ervan niet alle tooltips op de pagina opnieuw hoeft te renderen. Deze ref wordt alleen uitgelezen binnen de event handlers.
Waarom een timer van 200ms?
Als we een timer van minder dan 150ms kiezen, zal de tooltip waarschijnlijk openen wanneer een cursor er simpelweg overheen beweegt. Kiezen we meer dan 250ms, dan voelt de hover traag en defect aan. 200ms is in dit geval een goede, gebalanceerde waarde.
Oordeel en smaak
Als je AI vraagt om een tooltip te bouwen, zal het een volledig functionele tooltip maken. Maar uiteindelijk ben jij als mens degene die beslist of de gebouwde tooltip van voldoende kwaliteit is. Er zijn details die een "werkende" tooltip onderscheiden van een "gepolijste" tooltip, op dezelfde manier als dat een werkend product onderscheidt van een gepolijst product.
AI kan alleen een gepolijst product bouwen als je het daartoe stuurt. Dat kun je doen wanneer je smaak en oordeel hebt ontwikkeld, wat voortkomt uit jaren van ervaring, fouten en oefening. Je kunt wel gebruikmaken van de expertise van andere engineers of designers om hun standaard van een gepolijst product over te nemen, maar je bent uiteindelijk nog steeds degene die beoordeelt en beslist of het aan de standaarden voldoet om te worden gepubliceerd.
Skill: Tooltip Timing
Ik heb een Claude-skill gemaakt om het bovenstaande probleem op te lossen. Je kunt deze gebruiken om je eigen codebase te auditeren. Omdat elke codebase anders is en verschillende libraries gebruikt, is deze skill generiek geschreven.
Naam: tooltip Beschrijving: Tooltips hebben vertraging nodig zodat ze niet openen bij onbedoelde muisbewegingen.
Theorie
Een klik is altijd intentioneel, terwijl een hover dat niet is. De cursor beweegt over de pagina om ergens te komen en passeert onderweg elementen; een tooltip kan aan de hand van de hover alleen niet weten of de gebruiker hem echt wil openen. De 200ms is de manier om dit te bepalen.
.tooltip {
transition-delay: 200ms;
}
De drie waarden en statussen
| Waarde | Getal | Waarom |
| Open delay | 200ms | Onder de 150ms opent een cursor die alleen passeert de trigger al. Boven de 250ms voelt een bewuste hover traag aan. |
| Warm window | 300ms | Lang genoeg om de beweging van de ene trigger naar de volgende te dekken. Kort genoeg zodat een hover een seconde later opnieuw moet wachten. |
| Close delay | 0ms | Het verlaten van een trigger moet duidelijk zijn. Er is dus geen reden om te wachten. |
Gewenst gedrag
hover → wacht 200ms → tooltip opent (pagina is nu warm) leave → tooltip sluit → 300ms cooldown