Mouse, pointer & touch
Handle clicks, hovers, drags, pens, and touch screens with mouse events, pointer events, pointer capture, and HTML drag and drop.
- 01Read which mouse buttons are pressedTell
buttonfrombuttons, readrelatedTarget, and know the order of the events in one click. - 02Choose pointer eventsHandle a mouse, a pen, and a finger with one set of events, and know why mouse events still fire after them.
- 03Build dragging that doesn't breakUse pointer capture or HTML drag and drop, depending on what you build.
One set of events for every kind of pointer
People point at a web page in many ways. Some use a mouse. Some touch a phone screen with a finger. Some draw on a tablet with a pen. Whatever the device, the page needs to know three things: where is the pointer, what is it doing, and should the page react?
This lesson covers three groups of browser events:
- Mouse events, such as
clickandmouseover. You still use them for normal clicks, hover, and right-click menus. - Pointer events, such as
pointerdown. They work the same way for a mouse, a pen, and a finger. - Drag-and-drop events, such as
dragstartanddrop. The browser uses them to move an item into a drop area.
A universal remote has one set of buttons. The same buttons work your TV, your speaker, and your projector. Pointer events work the same way. You write one set of listeners, and they work for a mouse, a finger, and a pen. Each event also tells you which device sent it.
- In real life: One remote with the same buttons for every device
- In JavaScript: The same events for every device:
pointerdown,pointermove, andpointerup - In real life: A light on the remote that shows the device: TV, speaker, or projector
- In JavaScript:
pointerTypesaysmouse,pen, ortouch - In real life: An extra that only some devices have
- In JavaScript: Extra properties such as
pressure(how hard a pen presses), when the device reports them
A pointer is one point that points at or touches the page: a mouse cursor, a finger on the screen, or a pen tip. A pointer event is a browser event about that point. It says where the point is, what it is doing, and which device it came from.
const event = {
pointerType: "touch",
pointerId: 7,
isPrimary: true,
};
console.log(event.pointerType);
console.log(event.pointerId);
console.log(event.isPrimary);This code makes a small object shaped like a real pointer event. Lines 7 to 9 print three of its properties. In plain words, line by line:
- Line 2:
pointerTypeis"touch", so a finger made this event. - Line 3:
pointerIdis7. Every pointer on the screen gets its own id number. If two fingers touch the screen, each finger has a different id, so your code can tell them apart. - Line 4:
isPrimaryistrue. The first finger to touch the screen is the primary pointer. A drawing app can follow only the primary pointer and ignore a second finger.
The console prints touch, 7, and true.
mouseover vs mouseenter
INTERACTIVEHover events fire when the pointer crosses the edge of an element. There are two pairs of them, and they behave differently:
mouseoverandmouseoutbubble. Bubbling means an event on a child element also reaches its parent. So when the pointer moves from one child to another inside a card, the card hears a newmouseovereach time.mouseenterandmouseleavedo not bubble. The card hearsmouseenteronce when the pointer comes into the card, andmouseleaveonce when it goes out.
mouseover bubbles, so the card hears it again; mouseenter fired once when the pointer came into the card.mouseover and mouseout also have a relatedTarget property. It is the other element in the move: where the pointer came from, or where it is going. That helps when one listener on a parent handles many children (event delegation). But for a card with an icon, a title, and a button inside, all those extra events are just noise.
Picture a house with a bell at the front door and a small bell in every room. The front-door bell rings once when a visitor comes in. The room bells ring every time the visitor walks into another room. If you only care that someone came in, listen to the front-door bell. If you want to hear about every room, listen to the room bells.
- In real life: The front-door bell rings once when a visitor comes in
- In JavaScript:
mouseenterfires once when the pointer enters the element - In real life: A room bell rings each time the visitor walks into another room
- In JavaScript:
mouseoverfires again each time the pointer crosses into a child - In real life: Asking which room the visitor just left
- In JavaScript:
relatedTargetnames the element the pointer came from
const parent = document.querySelector(".card"); parent.addEventListener("mouseover", (event) => { console.log("over", event.target.className); console.log("from", event.relatedTarget?.className ?? "outside");}); parent.addEventListener("mouseenter", () => { console.log("entered the whole card");});Move the pointer slowly over the emoji, the title, and the button inside the card.
mouseover and mouseout bubble, so they fire again each time the pointer crosses into a child. mouseenter and mouseleave fire once, when the pointer enters or leaves the whole card.
relatedTarget when the browser gives one. Coming in from outside the card often shows outside.Pointer events have the same two pairs:
pointeroverandpointeroutbubble, likemouseoverandmouseout.pointerenterandpointerleavedo not bubble, likemouseenterandmouseleave.
Use the mouse names when your code only cares about a mouse. Use the pointer names when the same widget must also work with a pen or a finger.
Pointer events
UNIVERSALThese are the pointer events:
pointerdown,pointermove, andpointerup: press, move, and release.pointercancel: the browser stopped sending this pointer's events. You'll see why below.pointerover,pointerout,pointerenter, andpointerleave: the hover pairs from the last section.
They look like mouse events, but they carry extra properties about the device: pointerId, pointerType, isPrimary, pressure, and more.
After most pointer events, the browser also sends the matching mouse event, such as mousedown after pointerdown. These extra events are called compatibility mouse events. They exist so that older code written only for a mouse still works. So don't run the same action in both a pointerdown listener and a mousedown listener: it would run twice. For drawing, sliders, resizing, and gestures, pick pointer events.
pointercancel appearspointercancel means the browser stopped sending this pointer's events to your code. It often happens on a touch screen, when the browser decides the finger is scrolling or zooming the page. On your own drag area, the CSS rule touch-action: none tells the browser: "my code handles touch gestures here, so don't scroll."
Here is a common case. On a phone, a finger starts on your slider. Then it moves far enough that the browser thinks the user wants to scroll the page. The browser takes over, and your code gets pointercancel instead of pointerup. So clean up the drag in both cases, on pointerup and on pointercancel.
let dragging = true;
function finishGesture(eventType) {
dragging = false;
console.log(eventType, dragging);
}
finishGesture("pointercancel");Line 1 says a drag is going on: dragging is true. The finishGesture function on lines 3 to 6 sets dragging back to false. You can call it for either ending event. Line 8 calls it for pointercancel, so the console prints pointercancel false. Without this cleanup, a canceled slider can stay stuck, still looking as if it is being dragged.
When a control really needs the touch gesture for itself, add one CSS rule to it:
.custom-slider {
touch-action: none;
}The table below sums up the choices so far. Each row is one question your code might ask; read across to see what answers it.
| What do you need to know? | What to use | What it gives you | When to pick it |
|---|---|---|---|
| Which button changed? | event.button | A number: 0 main, 1 middle, 2 secondary. | When a button goes down or up and you need to know which one. |
| Which buttons are held now? | event.buttons | A sum: 1 main, 2 secondary, 4 middle. | During a move, to check that a button is still down. |
| Did the pointer move from one child to another? | mouseover / mouseout | These events bubble and include relatedTarget. | When one listener on a parent handles many children. |
| Did the pointer enter or leave the whole element? | mouseenter / mouseleave | These events do not bubble. | When moving between children should not start the hover again. |
| What is any pointer doing? | pointerdown / pointermove / pointerup | The same events for a mouse, a pen, and a finger, with pointerType. | For drawing, sliders, resizing, and gestures. |
| How do I move an item to a drop area? | HTML drag and drop | The events dragstart, dragover, and drop, plus dataTransfer. | For moving items with a mouse on a desktop. Touch support differs between devices. |
Pointer capture
INTERACTIVEDragging can break if the pointer has to stay exactly over the small thing you drag. Take a slider's thumb, the knob you drag. A fast hand can leave the thumb before the next move event arrives. Then the thumb stops getting moves, and it stops.
Pointer capture fixes this. When you call thumb.setPointerCapture(event.pointerId), every later event from that pointer goes to the thumb, even when the pointer is somewhere else. Capture ends by itself after pointerup. You can also end it yourself with releasePointerCapture().
When you fly a kite, the kite can drift far away from you, but the string still ties it to your hand. Pointer capture ties the pointer to the thumb in the same way. The pointer can move away, and the thumb still gets its events until you let go.
- In real life: Your hand holds the string
- In JavaScript: The element keeps getting that pointer’s events
- In real life: The kite flies far from your hand
- In JavaScript: The pointer moves outside the element
- In real life: You let go of the string
- In JavaScript:
pointeruporreleasePointerCapture()ends the capture
thumb.addEventListener("pointerdown", (event) => { thumb.setPointerCapture(event.pointerId); drag(event.clientX);}); thumb.addEventListener("pointermove", (event) => { if (event.buttons === 1) drag(event.clientX);}); thumb.addEventListener("pointerup", (event) => { thumb.releasePointerCapture(event.pointerId);});Capture is on. The thumb keeps getting this pointer's moves until pointerup, even when you drag outside the thumb.
touch-action: none, so a finger drag moves the slider instead of scrolling the page.thumb.addEventListener("pointerdown", (event) => { thumb.setPointerCapture(event.pointerId); drag(event.clientX);}); thumb.addEventListener("pointermove", (event) => { if (event.buttons === 1) drag(event.clientX);}); thumb.addEventListener("pointerup", (event) => { thumb.releasePointerCapture(event.pointerId);});Line 2 starts the capture when the button goes down. Line 7 moves the thumb only while the main button is held (buttons === 1). Line 11 ends the capture when the button comes up.
card.addEventListener("pointerdown", (event) => { card.setPointerCapture(event.pointerId); card.classList.add("dragging");}); card.addEventListener("pointerup", (event) => { card.releasePointerCapture(event.pointerId); card.classList.remove("dragging");});Line 2 captures the pointer when the press starts, so the drag keeps working outside the card. Line 3 adds the dragging class, which only changes how the card looks. A real app would also move the card in a pointermove listener. Line 7 releases the capture when the press ends. Always release capture when the gesture ends or is canceled.
Drag and drop
INTERACTIVEHTML drag and drop is a different set of events. It moves data from the item you drag (the source) to the place you drop it (the target). It takes four steps:
- Mark the source as draggable:
draggable="true"in HTML, orcard.draggable = truein JavaScript. - In the source's
dragstartlistener, save some data withevent.dataTransfer.setData().dataTransferis the object that carries data during a drag. - In the target's
dragoverlistener, callpreventDefault(). This tells the browser the target accepts drops. Many apps do the same indragenter. - In the target's
droplistener, read the data withgetData()and move the item.
While the drag is going on, the browser protects the data so that other code can't read it. That is why you can read your data in drop, but not in dragover.
preventDefault() or the drop never happens.Movers won't leave a box at a house unless someone there says "yes, deliver it here." A drop target is the same: it must say yes. It says yes by calling preventDefault() in dragover. This is the step people forget most often.
- In real life: The movers pack a box
- In JavaScript:
dragstartsaves the card’s id withdataTransfer.setData() - In real life: The new home says “yes, deliver here”
- In JavaScript:
dragovercallspreventDefault()to accept the drop - In real life: The movers unpack the box
- In JavaScript:
dropreads the id and moves the element - In real life: The movers finish the job
- In JavaScript:
dragendcleans up the look, whether the drop worked or not
card.draggable = true; card.addEventListener("dragstart", (event) => { event.dataTransfer.setData("text/plain", card.id);}); target.addEventListener("dragover", (event) => { event.preventDefault();}); target.addEventListener("drop", (event) => { const id = event.dataTransfer.getData("text/plain"); target.append(document.getElementById(id));});Use a mouse or a trackpad. Many touch screens don't run HTML drag and drop the same way.
Drag a card to another list.
card.draggable = true; card.addEventListener("dragstart", (event) => { event.dataTransfer.setData("text/plain", card.id);}); target.addEventListener("dragover", (event) => { event.preventDefault();}); target.addEventListener("drop", (event) => { const id = event.dataTransfer.getData("text/plain"); target.append(document.getElementById(id));});Line 1 makes the card draggable. Line 4 saves the card's id when the drag starts. Line 8 accepts the drop on the target. Lines 12 and 13 read the id and move the card into the target.
Where you’ll use this
PRACTICALReal apps often mix these events:
- A drawing app uses pointer events, so it works with a mouse, a pen, and a finger.
- A splitter, the bar you drag to resize two panels, captures the pointer. It keeps resizing even when your hand moves faster than the bar.
- A file manager might use HTML drag and drop on a desktop, and pointer events on touch screens.
element.addEventListener("pointerdown", (event) => { if (event.button !== 0) return; element.setPointerCapture(event.pointerId);}); element.addEventListener("pointermove", (event) => { if (event.buttons !== 1) return; drawLineTo(event.clientX, event.clientY);});Line 2 ignores any press that is not the main button (button is not 0). Line 3 captures the pointer. Line 7 skips moves when the main button is not held. Line 8 draws to the pointer's position.
const canvas = document.querySelector("canvas");const context = canvas.getContext("2d"); canvas.addEventListener("pointerdown", (event) => { context.beginPath(); context.moveTo(event.offsetX, event.offsetY);}); canvas.addEventListener("pointermove", (event) => { if (event.buttons !== 1) return; context.lineTo(event.offsetX, event.offsetY); context.stroke();});A <canvas> is an HTML element that JavaScript can draw on. This code works the same for a finger, a mouse, or a pen:
- Lines 4 to 7:
pointerdownstarts a new line at the pointer's spot. - Lines 9 to 13:
pointermoveadds to the line, but only while the main button is held (line 10). - The page's CSS gives the canvas
touch-action: none, so a finger draws instead of scrolling the page.
A full version would also listen for pointerup and pointercancel to end the line.
- Highlight a menu once when the pointer enters it, even though the menu holds icons
- Draw with a mouse, a finger, or a pen
- Move cards between lists, with the browser showing where they can drop
- Open your own right-click menu
- Make a brush line thicker when a pen presses harder
- Let a card carry its id to a drop area
- Show a tooltip once when the pointer enters a button that holds an icon
- Run a special action when the middle button (the wheel) is clicked
Put each feature in the group of events it needs most.
Common misconceptions
- "Pointer events are the CSS
pointer-eventsproperty." No. Pointer events are JavaScript events, such aspointerdown. The CSS propertypointer-eventsis something else: it decides whether an element can be clicked or hovered at all. - "
buttonandbuttonsmean the same thing." No.buttonis the one button that changed.buttonsadds up every button held right now. - "
mouseenterbubbles likemouseover." It does not. A parent does not hearmouseenterfrom its children. If one listener on a parent should handle many children, usemouseover. - "Pointer capture captures every pointer." No. It captures one pointer, chosen by its
pointerId, for one element. - "HTML drag and drop works the same on every touch screen." No. Touch support differs between devices. Test on real phones, and think about using pointer events for touch dragging.
- "A
droplistener is enough." No. The browser normally firesdroponly on a target that calledpreventDefault()duringdragover.
Practice exercises
5 EXERCISESconst button = 2;
const buttons = 3;
console.log(button);
console.log(buttons);2
3Line 3 prints button, which is 2. Line 4 prints buttons, which is 3: the main button (1) plus the secondary button (2).
Use mouseenter for mouse-only code, or pointerenter for a widget that also works with a pen or a finger. Neither one bubbles, so moving between the children doesn't start the hover again.
preventDefault() in dragover to accept drops. This small program pretends to do that step. What does it print?let canDrop = false;
const event = { preventDefault() { canDrop = true; } };
event.preventDefault();
console.log(canDrop);let canDrop = false;
const event = { preventDefault() { canDrop = true; } };
event.preventDefault();
console.log(canDrop);Calling preventDefault() during dragover tells the browser this target accepts a drop. In this small program, the pretend preventDefault() sets canDrop to true before line 4 prints it, so the console prints true.
pointerdown listener call?thumb.setPointerCapture(event.pointerId);Capture sends every later event from that pointer to the thumb, so the drag keeps working even outside the thumb.
clientX is the pointer's distance in pixels from the left edge of the window. A slider track starts at clientX 100 and is 200 pixels wide. The pointer is at clientX 175. What percent does the program print?function percent(clientX, left, width) {
const raw = ((clientX - left) / width) * 100;
return Math.max(0, Math.min(100, Math.round(raw)));
}
console.log(percent(175, 100, 200));function percent(clientX, left, width) {
const raw = ((clientX - left) / width) * 100;
return Math.max(0, Math.min(100, Math.round(raw)));
}
console.log(percent(175, 100, 200));The pointer is 75 pixels into a 200-pixel track. That is 37.5%. Math.round turns it into 38. Math.max and Math.min keep the answer between 0 and 100.
Check your understanding
7 QUESTIONSQuestion 1 of 7In most browsers, which is true for a normal click with the main mouse button?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconst event = { button: 1, buttons: 4 }; console.log(event.button); console.log(event.buttons);Choose an answer to see the explanation.
Question 3 of 7Why can
mouseoverfire several times while the pointer stays inside one card?Choose an answer to see the explanation.
Question 4 of 7What should you set on a custom touch slider so the browser does not take over the drag to scroll the page?
Choose an answer to see the explanation.
Question 5 of 7This program pretends to be a
dragoverlistener. What does it print?Read the code, then predictlet allowed = false; const event = { preventDefault() { allowed = true; } }; event.preventDefault(); console.log(allowed);Choose an answer to see the explanation.
Question 6 of 7When can
dataTransfer.getData()read your own drag data?Choose an answer to see the explanation.
Question 7 of 7Which event should you listen for to build your own right-click menu?
Choose an answer to see the explanation.
Key takeaways
- Mouse events still matter for clicks, hover, other-button clicks (
auxclick), and right-click menus (contextmenu). buttonsays which button changed.buttonssays which buttons are held now.mouseoverandmouseoutbubble.mouseenterandmouseleavedo not.- Pick pointer events for custom controls that must work with a mouse, a pen, and a finger.
- Pointer capture sends one pointer's events to one element for the whole drag.
- HTML drag and drop needs
preventDefault()indragover, and it works differently on touch screens.
Final definition: Pointer events are browser events about one mouse, pen, or touch point. They use the same set of events for every device.
Up next: Keyboard & focus.