While we've long had the ability to detect mouse events with JavaScript, the web was missing a singular interface to detect interaction regardless of input type. As of the release of Safari 13 in September 2019, the pointer events API became the cross-browser, unifying solution.
As MDN docs describe, a pointer "is a hardware-agnostic device that can target a specific set of screen coordinates." Generally, you can consider pointers to include mouse, pen/stylus, or touch interactions.
Pointer events inherit from the mouse events interface, so you receive corresponding events, such as:
mousedown
/pointerdown
mouseup
/pointerup
mousemove
/pointermove
For backwards compatibility, the browser may opt to map these generic events to mouse events, with some exceptions.
Additional specific pointer event properties include those that tell about the dimensions of the pointer (width
and height
), the pressure being applied, the pointer's tilt, its type, a unique pointer id, and whether it's the primary pointer of the type.
Using pointer events to resize UI
#To explore how these properties work together and learn two additional pointer methods, we'll be building "Santa's Workshop Handbook" - an online reference for elves and other workshop workers!
The handbook shares a need found in many documentation sites or multi-column application windows: the ability to resize one or more panels to reveal the content or hide it away for more reading or work space.
Fundamentally, the expected interaction is "click and drag," but pointer events will let us write this to be touch-friendly, too! We'll also ensure the feature is accessible for keyboard users.
First, we'll add the basic HTML and CSS for the handbook.
Handbook HTML and CSS
.demo {
padding: 0 !important;
}
.workshop-handbook {
--panel-width: 200px;
font-family: system-ui;
font-size: 1.25rem;
display: grid;
align-content: start;
grid-template-columns: var(--panel-width) 1fr;
position: relative;
background-color: firebrick;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cfilter id='paper' x='0%25' y='0%25' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.11' result='noise' numOctaves='5' /%3E%3CfeDiffuseLighting in='noise' lighting-color='firebrick' surfaceScale='1.5'%3E%3CfeDistantLight azimuth='85' elevation='70' /%3E%3C/feDiffuseLighting%3E%3C/filter%3E%3C/defs%3E%3Crect x='0' y='0' width='100%25' height='100%25' filter='url(%23paper)'/%3E%3C/svg%3E");
overflow: hidden;
}
.workshop-handbook * {
margin: 0;
}
.workshop-handbook :is(nav, article) {
all: unset;
max-height: 60vh;
max-height: 60dvh;
overflow-y: auto;
overscroll-behavior: contain;
}
.workshop-handbook nav {
padding: 1rem;
}
.workshop-handbook nav ol {
padding-left: 0;
display: grid;
gap: 1em;
}
.workshop-handbook nav li {
color: white;
/* force the overflow and add ellipsis */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.workshop-handbook nav a {
font-family: Papyrus;
text-decoration: none;
color: white;
position: relative;
padding-left: 1em;
}
.workshop-handbook nav a:hover {
text-decoration: underline;
text-underline-offset: 0.15em;
}
.workshop-handbook nav a:focus-visible {
outline: none;
}
.workshop-handbook nav a:focus-visible::before {
content: "";
position: absolute;
top: calc(50% - 0.5em);
left: 0;
border: 0.5em solid transparent;
border-left-color: currentColor;
}
.workshop-handbook article {
padding: clamp(1.25rem, 5%, 3rem);
background-color: #edf9f6;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cfilter id='paper' x='0%25' y='0%25' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.11' result='noise' numOctaves='5' /%3E%3CfeDiffuseLighting in='noise' lighting-color='%23edf9f6' surfaceScale='1.5'%3E%3CfeDistantLight azimuth='85' elevation='70' /%3E%3C/feDiffuseLighting%3E%3C/filter%3E%3C/defs%3E%3Crect x='0' y='0' width='100%25' height='100%25' filter='url(%23paper)'/%3E%3C/svg%3E");
line-height: 1.5;
font-family: Chalkboard, Comic Sans MS;
}
.workshop-handbook article :not(h2) {
color: #222;
}
.workshop-handbook article * + * {
margin-top: 1em;
}
.workshop-handbook article h2 {
color: firebrick;
font-family: Papyrus;
font-weight: normal;
}
.workshop-handbook .visually-hidden {
clip-path: inset(50%);
height: 1px;
width: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
@media (forced-colors: active) {
.workshop-handbook :is(nav, article) {
background-image: none;
}
}
π Welcome to the Workshop! π
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod fugiat autem ab? Veritatis?
Quisquam omnis mollitia assumenda enim? Eum dolorum unde quasi doloremque perferendis velit in.
Cumque qui dolore voluptatibus, reiciendis ipsam optio. Ad nihil repudiandae sunt ratione officiis.
Necessitatibus modi blanditiis earum suscipit reiciendis deserunt, placeat, quis perspiciatis, ullam natus magnam?
Soluta aspernatur exercitationem iste deserunt? Exercitationem debitis modi maxime nulla officiis facilis eius.
We've included the --panel-width
custom property for the handbook, which we'll be manipulating based on the pointer's position relative to the amount the panel has been moved.
Pointer target element
#What's missing in the markup so far is a target element to attach pointer events. In this case, a <button>
is appropriate since it can receive focus and is expected to handle custom events.
Receiving focus is an important aspect since we want our resize trigger to be keyboard accessible, and that's the first step. That also means we need to consider where it makes sense in the focus order. I've decided to place it as the first focus target in the nav so that it's immediately discoverable for keyboard users.
<nav>
<button type="button" class="resize-trigger" aria-describedby="resize-description"><span class="visually-hidden">Resize table of contents</span></button>
<p hidden id="resize-description">Drag or use left and right arrow keys.</p>
Additionally, this is a custom control, and it doesn't quite map to any ARIA attributes. It's not a "drag and drop" since there is no drop target, so we've added two important features to clarify the function.
The first is the button label itself which we've made "visually hidden" meaning users of assistive technology like screen readers will have the label announced even though it's not visible. Second, we've included a bit more instructions in the [hidden]
paragraph which has a unique id
that's been referenced within the button's aria-describedby
attribute. Assistive tech will announce something like "Resize table of contents, button, navigation (short pause) Drag or use left and right arrow keys." The "navigation" noted in the announcement is due to being the first focus target in the landmark <nav>
element.
The following demo has been updated to include the .resize-trigger
elements and styles and is visible on :hover
and :focus
. We've also included the related CSS media query to detect a coarse
pointer. This will be often (but not always) be true for touch devices, or when a mouse is not present or not the primary pointing device. This style makes it always visible so it is easier to find and manipulate without a mouse.
Resize trigger element and CSS
.workshop-handbook {
--resize-width: 0.75rem;
}
.workshop-handbook .resize-trigger {
--resize-bg: linear-gradient(to right, #e4b81e, #9f8837);
all: unset;
outline: none;
position: absolute;
top: 0;
left: var(--panel-width);
width: var(--resize-width);
height: 100%;
cursor: ew-resize;
z-index: 1;
}
.workshop-handbook .resize-trigger:is(:hover, :focus) {
/* Ensure visibility in forced-colors mode which removes the gradient */
background-color: Highlight;
background-image: var(--resize-bg);
box-shadow: 0 0 5px 1px #f5e096;
outline: 1px solid transparent;
}
@media (pointer: coarse) {
.workshop-handbook .resize-trigger {
background-image: var(--resize-bg);
}
}
π Welcome to the Workshop! π
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod fugiat autem ab? Veritatis?
Quisquam omnis mollitia assumenda enim? Eum dolorum unde quasi doloremque perferendis velit in.
Cumque qui dolore voluptatibus, reiciendis ipsam optio. Ad nihil repudiandae sunt ratione officiis.
Necessitatibus modi blanditiis earum suscipit reiciendis deserunt, placeat, quis perspiciatis, ullam natus magnam?
Soluta aspernatur exercitationem iste deserunt? Exercitationem debitis modi maxime nulla officiis facilis eius.
Using the pointer events API
#Now that the HTML is in place, it's time to make the resize trigger work with pointer events!
The basic flow is as follows:
- On
pointerdown
, start tracking theclientX
position duringonpointermove
. Additionally, usesetPointerCapture()
to continue tracking only this pointer whilepointerdown
is active. - On
pointerup
, remove theonpointermove
event to end the drag functionality and stop tracking the pointer withreleasePointerCapture()
.
The resizePanel()
script receives the clientX
position or other provided integer and compares it to the boundary of the handbook and a minimum and maximum size we'll allow for the panel. If the number is an allowed size, it updates the --panel-width
CSS custom property which ultimately is responsible for the resizing since it sets the grid column width for the navigation.
Finally, for purposes of our application, we listen for keydown
so that we can detect the ArrowLeft
and ArrowRight
keys to allow keyboard users to resize the panel, too.
Resizing pointer events
const handbook = document.getElementById("handbook");
const panelResize = handbook.querySelector(".resize-trigger");
const handbookBoundaries = handbook.getBoundingClientRect();
const rightBoundary = handbookBoundaries.right;
const leftBoundary = handbookBoundaries.left;
const contentMin = 150;
const resizePanel = (size) => {
if (size >= contentMin && size <= rightBoundary - leftBoundary - contentMin) {
handbook.style.setProperty("--panel-width", `${size}px`);
}
};
// Create drag and move interaction
panelResize.addEventListener("pointerdown", ({ pointerId }) => {
panelResize.onpointermove = ({ clientX }) => {
// Adjust position since demo is not a fullwidth window application
const panelPos = clientX - leftBoundary;
resizePanel(panelPos);
};
panelResize.setPointerCapture(pointerId);
});
// Release resizer when drag ends / mouse is disengaged
panelResize.addEventListener("pointerup", ({ pointerId }) => {
panelResize.onpointermove = null;
panelResize.releasePointerCapture(pointerId);
});
// Allow keyboard resizing
panelResize.addEventListener("keydown", ({ key }) => {
if (["ArrowLeft", "ArrowRight"].includes(key)) {
// get current value and add or remove 10px
let posX = parseInt(
getComputedStyle(handbook).getPropertyValue("--panel-width")
);
if (key === "ArrowLeft") {
posX -= 10;
} else {
posX += 10;
}
resizePanel(posX);
}
});
π Welcome to the Workshop! π
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod fugiat autem ab? Veritatis?
Quisquam omnis mollitia assumenda enim? Eum dolorum unde quasi doloremque perferendis velit in.
Cumque qui dolore voluptatibus, reiciendis ipsam optio. Ad nihil repudiandae sunt ratione officiis.
Necessitatibus modi blanditiis earum suscipit reiciendis deserunt, placeat, quis perspiciatis, ullam natus magnam?
Soluta aspernatur exercitationem iste deserunt? Exercitationem debitis modi maxime nulla officiis facilis eius.
Additional resources
#Learn more about pointer events from the following resources:
- Pointer events API docs on MDN
- Also see the pointer events docs as related to canvas and a drawing app demo
- Docs for `setPointerCapture()` show an additional example of changing an element's position