Touch
Angular Bootstrap 5 Touch
Use native Pointer Events to handle pinch, swipe, tap, press, pan, and rotate gestures without an additional gesture library.
Note: Read the API tab to find all available options and advanced customization
Note: Pointer Events support touch, pen, and mouse input. Add an equivalent keyboard action when the gesture performs an action that keyboard users also need.
Configuration
Angular 22 no longer provides the deprecated HammerJS integration. The examples on this page use the following standalone directive to translate native Pointer Events into gesture events. No additional package is required.
import {
Directive,
ElementRef,
EventEmitter,
HostBinding,
HostListener,
Input,
OnDestroy,
Output,
} from '@angular/core';
interface PointerState {
startX: number;
startY: number;
x: number;
y: number;
startTime: number;
}
interface PanEvent {
deltaX: number;
deltaY: number;
preventDefault: () => void;
}
@Directive({
selector: '[mdbPointerGestures]',
standalone: true,
})
export class PointerGesturesDirective implements OnDestroy {
@Input() pressDuration = 250;
@Input() doubleTapInterval = 500;
@Input() tapMaxDistance = 10;
@Input() swipeThreshold = 50;
@Input() swipeMaxDuration = 500;
@Output() press = new EventEmitter<void>();
@Output() tap = new EventEmitter<{ x: number; y: number }>();
@Output() doubleTap = new EventEmitter<{ x: number; y: number }>();
@Output() panstart = new EventEmitter<void>();
@Output() panmove = new EventEmitter<PanEvent>();
@Output() panleft = new EventEmitter<PanEvent>();
@Output() panright = new EventEmitter<PanEvent>();
@Output() panup = new EventEmitter<PanEvent>();
@Output() pandown = new EventEmitter<PanEvent>();
@Output() pinch = new EventEmitter<{
ratio: number;
origin: { x: number; y: number };
}>();
@Output() swipeleft = new EventEmitter<void>();
@Output() swiperight = new EventEmitter<void>();
@Output() swipeup = new EventEmitter<void>();
@Output() swipedown = new EventEmitter<void>();
@Output() rotate = new EventEmitter<{ currentAngle: number }>();
@HostBinding('style.touch-action') touchAction = 'none';
private pointers = new Map<number, PointerState>();
private pressTimer: ReturnType<typeof setTimeout> | undefined;
private pressFired = false;
private panStarted = false;
private lastTapTime = 0;
private initialPinchDistance = 0;
private initialRotation = 0;
constructor(private elementRef: ElementRef<HTMLElement>) {}
@HostListener('pointerdown', ['$event'])
onPointerDown(event: PointerEvent): void {
if (event.pointerType === 'mouse' && event.button !== 0) {
return;
}
this.elementRef.nativeElement.setPointerCapture(event.pointerId);
this.pointers.set(event.pointerId, {
startX: event.clientX,
startY: event.clientY,
x: event.clientX,
y: event.clientY,
startTime: event.timeStamp,
});
if (this.pointers.size === 1) {
this.pressFired = false;
this.panStarted = false;
this.pressTimer = setTimeout(() => {
this.pressFired = true;
this.press.emit();
}, this.pressDuration);
} else if (this.pointers.size === 2) {
this.cancelPress();
const [first, second] = Array.from(this.pointers.values());
this.initialPinchDistance = this.getDistance(first, second);
this.initialRotation = this.getAngle(first, second);
}
}
@HostListener('pointermove', ['$event'])
onPointerMove(event: PointerEvent): void {
const pointer = this.pointers.get(event.pointerId);
if (!pointer) {
return;
}
pointer.x = event.clientX;
pointer.y = event.clientY;
if (this.pointers.size === 2) {
this.emitTwoPointerGestures();
return;
}
const deltaX = pointer.x - pointer.startX;
const deltaY = pointer.y - pointer.startY;
if (Math.hypot(deltaX, deltaY) > this.tapMaxDistance) {
this.cancelPress();
}
if (!this.panStarted) {
this.panStarted = true;
this.panstart.emit();
}
const panEvent: PanEvent = {
deltaX,
deltaY,
preventDefault: () => event.preventDefault(),
};
event.preventDefault();
this.panmove.emit(panEvent);
if (Math.abs(deltaX) >= Math.abs(deltaY)) {
deltaX < 0 ? this.panleft.emit(panEvent) : this.panright.emit(panEvent);
} else {
deltaY < 0 ? this.panup.emit(panEvent) : this.pandown.emit(panEvent);
}
}
@HostListener('pointerup', ['$event'])
onPointerUp(event: PointerEvent): void {
const pointer = this.pointers.get(event.pointerId);
if (!pointer) {
return;
}
pointer.x = event.clientX;
pointer.y = event.clientY;
this.cancelPress();
if (this.pointers.size === 1) {
this.emitTap(pointer, event);
this.emitSwipe(pointer, event);
}
this.removePointer(event.pointerId);
}
@HostListener('pointercancel', ['$event'])
onPointerCancel(event: PointerEvent): void {
this.cancelPress();
this.removePointer(event.pointerId);
}
ngOnDestroy(): void {
this.cancelPress();
}
private emitTap(pointer: PointerState, event: PointerEvent): void {
const distance = Math.hypot(
event.clientX - pointer.startX,
event.clientY - pointer.startY
);
if (this.pressFired || distance > this.tapMaxDistance) {
return;
}
const origin = { x: event.clientX, y: event.clientY };
const timeSinceLastTap = event.timeStamp - this.lastTapTime;
this.tap.emit(origin);
if (timeSinceLastTap > 0 && timeSinceLastTap <= this.doubleTapInterval) {
this.doubleTap.emit(origin);
this.lastTapTime = 0;
} else {
this.lastTapTime = event.timeStamp;
}
}
private emitSwipe(pointer: PointerState, event: PointerEvent): void {
const deltaX = event.clientX - pointer.startX;
const deltaY = event.clientY - pointer.startY;
const duration = event.timeStamp - pointer.startTime;
if (
duration > this.swipeMaxDuration ||
Math.max(Math.abs(deltaX), Math.abs(deltaY)) < this.swipeThreshold
) {
return;
}
if (Math.abs(deltaX) >= Math.abs(deltaY)) {
deltaX < 0 ? this.swipeleft.emit() : this.swiperight.emit();
} else {
deltaY < 0 ? this.swipeup.emit() : this.swipedown.emit();
}
}
private emitTwoPointerGestures(): void {
const [first, second] = Array.from(this.pointers.values());
const distance = this.getDistance(first, second);
const angle = this.getAngle(first, second);
const bounds = this.elementRef.nativeElement.getBoundingClientRect();
if (this.initialPinchDistance > 0) {
this.pinch.emit({
ratio: distance / this.initialPinchDistance,
origin: {
x: (first.x + second.x) / 2 - bounds.left,
y: (first.y + second.y) / 2 - bounds.top,
},
});
}
this.rotate.emit({ currentAngle: (angle - this.initialRotation) / 360 });
}
private getDistance(first: PointerState, second: PointerState): number {
return Math.hypot(first.x - second.x, first.y - second.y);
}
private getAngle(first: PointerState, second: PointerState): number {
return Math.atan2(second.y - first.y, second.x - first.x) * (180 / Math.PI);
}
private cancelPress(): void {
clearTimeout(this.pressTimer);
this.pressTimer = undefined;
}
private removePointer(pointerId: number): void {
if (this.elementRef.nativeElement.hasPointerCapture(pointerId)) {
this.elementRef.nativeElement.releasePointerCapture(pointerId);
}
this.pointers.delete(pointerId);
if (this.pointers.size === 0) {
this.panStarted = false;
this.initialPinchDistance = 0;
this.initialRotation = 0;
}
}
}
import { Component } from '@angular/core';
import { PointerGesturesDirective } from './pointer-gestures.directive';
@Component({
selector: 'app-root',
imports: [PointerGesturesDirective],
templateUrl: './app.component.html',
})
export class AppComponent {}
Press
Press calls the chosen method when the pointer remains down on the element for more than 250 milliseconds.
Hold the button to remove the mask from the image
<div>
<div class="bg-image">
<img src="https://mdbootstrap.com/img/new/standard/city/053.webp" class="img-fluid" />
@if (showMask) {
<div
class="mask"
style="background-color: rgba(0, 0, 0, 0.6)"
id="remove-bg"
>
<div class="d-flex justify-content-center align-items-center h-100">
<p id="press-text" class="text-white mb-0">
Hold the button to remove the mask from the image
</p>
</div>
</div>
}
</div>
<div class="my-3">
<button
mdbPointerGestures
type="button"
class="btn btn-primary btn-press"
(press)="onPress()"
>
Tap & hold to show image
</button>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
showMask = true;
constructor() {}
onPress() {
this.showMask = false;
}
}
Press duration
Set the pressDuration input to change the default press duration.
Hold the button for 5s to remove mask from the image
<div>
<div class="bg-image">
<img src="https://mdbootstrap.com/img/new/standard/city/053.webp" class="img-fluid" />
@if (showMask) {
<div
class="mask"
style="background-color: rgba(0, 0, 0, 0.6)"
id="remove-bg"
>
<div class="d-flex justify-content-center align-items-center h-100">
<p id="press-text" class="text-white mb-0">
Hold the button for 5s to remove mask from the imag
</p>
</div>
</div>
}
</div>
<div class="my-3">
<button
mdbPointerGestures
type="button"
class="btn btn-primary btn-press"
[pressDuration]="5000"
(press)="onPress()"
>
Tap & hold to show image
</button>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
showMask = true;
constructor() {}
onPress() {
this.showMask = false;
}
}
Tap
The callback on tap event is called with an object containing origin field - the x and y coordinates of the user's touch.
Tap button to change a color
<div>
<div class="bg-image">
<img src="https://mdbootstrap.com/img/new/standard/city/053.webp" class="img-fluid" />
<div class="mask" [ngStyle]="{ 'background-color': background }" id="bg-tap">
<div class="d-flex justify-content-center align-items-center h-100">
<p class="text-white mb-0">Tap button to change a color</p>
</div>
</div>
</div>
<div class="my-3">
<button
mdbPointerGestures
type="button"
class="btn btn-primary btn-tap"
(tap)="onTap()"
>
Tap to change a color
</button>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
background = 'rgba(0, 0, 0, 0.6)';
constructor() {}
generateColor(): string {
return `rgba(${this.getRandomNumber()},${this.getRandomNumber()},${this.getRandomNumber()},.4)`;
}
getRandomNumber(): number {
return Math.floor(Math.random() * 255) + 1;
}
onTap() {
this.background = this.generateColor();
}
}
Double Tap
Set default taps to touch event.
Change background color with 2 taps
<div>
<div class="bg-image">
<img src="https://mdbootstrap.com/img/new/standard/city/053.webp" class="img-fluid" />
<div class="mask" [ngStyle]="{ 'background-color': background }" id="bg-tap">
<div class="d-flex justify-content-center align-items-center h-100">
<p class="text-white mb-0">Tap button to change a color</p>
</div>
</div>
</div>
<div class="my-3">
<button
mdbPointerGestures
type="button"
class="btn btn-primary btn-tap"
(doubleTap)="onTap()"
>
Tap to change a color
</button>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
background = 'rgba(0, 0, 0, 0.6)';
constructor() {}
generateColor(): string {
return `rgba(${this.getRandomNumber()},${this.getRandomNumber()},${this.getRandomNumber()},.4)`;
}
getRandomNumber(): number {
return Math.floor(Math.random() * 255) + 1;
}
onTap() {
this.background = this.generateColor();
}
}
Pan
The pan event is useful for dragging elements. While the pointer moves, the directive emits the horizontal and vertical distance from its starting position.
<div>
<div class="bg-image" id="pan">
<img
mdbPointerGestures
[ngStyle]="{ transform: panTransform }"
(panstart)="onPanStart()"
(panmove)="onPanMove($event)"
src="https://mdbootstrap.com/img/new/standard/city/053.webp"
class="img-fluid"
id="img-pan"
/>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
x = 0;
y = 0;
startX = 0;
startY = 0;
panTransform: string;
constructor() {}
onPanStart() {
this.startX = this.x;
this.startY = this.y;
}
onPanMove(event: any) {
event.preventDefault();
this.x = this.startX + event.deltaX;
this.y = this.startY + event.deltaY;
this.panTransform = this.getTransform(this.x, this.y);
}
getTransform(x: number, y: number): string {
return `translate(${x}px, ${y}px)`;
}
}
Pan Left
Pan with only left direction
<div>
<div class="bg-image" id="pan">
<img
mdbPointerGestures
[ngStyle]="{ transform: panTransform }"
(panstart)="onPanStart()"
(panleft)="onPanLeft($event)"
src="https://mdbootstrap.com/img/new/standard/city/053.webp"
class="img-fluid"
id="img-pan"
/>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
x = 0;
y = 0;
startX = 0;
startY = 0;
panTransform: string;
constructor() {}
onPanStart() {
this.startX = this.x;
this.startY = this.y;
}
onPanLeft(event: any) {
event.preventDefault();
this.x = this.startX + event.deltaX;
this.panTransform = this.getTransform(this.x, 0);
}
getTransform(x: number, y: number): string {
return `translate(${x}px, ${y}px)`;
}
}
Pan Right
Pan with only right direction
<div>
<div class="bg-image" id="pan">
<img
mdbPointerGestures
[ngStyle]="{ transform: panTransform }"
(panstart)="onPanStart()"
(panright)="onPanRight($event)"
src="https://mdbootstrap.com/img/new/standard/city/053.webp"
class="img-fluid"
id="img-pan"
/>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
x = 0;
y = 0;
startX = 0;
startY = 0;
panTransform: string;
constructor() {}
onPanStart() {
this.startX = this.x;
this.startY = this.y;
}
onPanRight(event: any) {
event.preventDefault();
this.x = this.startX + event.deltaX;
this.panTransform = this.getTransform(this.x, 0);
}
getTransform(x: number, y: number): string {
return `translate(${x}px, ${y}px)`;
}
}
Pan Up/Down
Pan with only up/down direction
<div>
<div class="bg-image" id="pan">
<img
mdbPointerGestures
[ngStyle]="{ transform: panTransform }"
(panstart)="onPanStart()"
(panup)="onPanUp($event)"
(pandown)="onPanDown($event)"
src="https://mdbootstrap.com/img/new/standard/city/053.webp"
class="img-fluid"
id="img-pan"
/>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
x = 0;
y = 0;
startX = 0;
startY = 0;
panTransform: string;
constructor() {}
onPanStart() {
this.startX = this.x;
this.startY = this.y;
}
onPanUp(event: any) {
event.preventDefault();
this.y = this.startY + event.deltaY;
this.panTransform = this.getTransform(0, this.y);
}
onPanDown(event: any) {
event.preventDefault();
this.y = this.startY + event.deltaY;
this.panTransform = this.getTransform(0, this.y);
}
getTransform(x: number, y: number): string {
return `translate(${x}px, ${y}px)`;
}
}
Pinch
The pinch event provides the ratio between the current and initial pointer distance. It also provides the midpoint between the pointers, which can be used as the transform origin.
<div>
<div class="bg-image" id="pan">
<img
mdbPointerGestures
[ngStyle]="{
transform: pinchTransform,
'transform-origin': pinchTransformOrigin
}"
(pinch)="onPinch($event)"
src="https://mdbootstrap.com/img/new/standard/city/053.webp"
class="img-fluid"
/>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
pinchTransform: string;
pinchTransformOrigin: string;
constructor() {}
onPinch(event: any) {
this.pinchTransform = `scale(${event.ratio})`;
this.pinchTransformOrigin = `${event.origin.x}px ${event.origin.y}px`;
}
}
Swipe Left/Right
The directive compares the pointer's start and end positions and emits an event for the detected swipe direction.
This example shows example with left and right
Swipe Left-Right to change a color
<div>
<div
mdbPointerGestures
class="bg-image"
(swiperight)="onSwipeRight()"
(swipeleft)="onSwipeLeft()"
>
<img src="https://mdbootstrap.com/img/new/standard/city/053.webp" class="img-fluid" />
<div
class="mask"
[ngStyle]="{ 'background-color': background }"
id="swipe-left-right"
>
<div class="d-flex justify-content-center align-items-center h-100">
<p class="text-white mb-0">Swipe Left-Right to change a color</p>
</div>
</div>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
background = 'rgba(0, 0, 0, 0.6)';
constructor() {}
generateColor(): string {
return `rgba(${this.getRandomNumber()},${this.getRandomNumber()},${this.getRandomNumber()},.4)`;
}
getRandomNumber(): number {
return Math.floor(Math.random() * 255) + 1;
}
onSwipeLeft() {
this.background = this.generateColor();
}
onSwipeRight() {
this.background = this.generateColor();
}
}
Swipe Up/Down
Bind the swipeup and swipedown outputs to handle vertical swipes.
Swipe Up-Down to change a color
<div>
<div
mdbPointerGestures
class="bg-image"
(swipeup)="onSwipeUp()"
(swipedown)="onSwipeDown()"
>
<img src="https://mdbootstrap.com/img/new/standard/city/053.webp" class="img-fluid" />
<div
class="mask"
[ngStyle]="{ 'background-color': background }"
id="swipe-left-right"
>
<div class="d-flex justify-content-center align-items-center h-100">
<p class="text-white mb-0">Swipe Up-Down to change a color</p>
</div>
</div>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
background = 'rgba(0, 0, 0, 0.6)';
constructor() {}
generateColor(): string {
return `rgba(${this.getRandomNumber()},${this.getRandomNumber()},${this.getRandomNumber()},.4)`;
}
getRandomNumber(): number {
return Math.floor(Math.random() * 255) + 1;
}
onSwipeUp() {
this.background = this.generateColor();
}
onSwipeDown() {
this.background = this.generateColor();
}
}
Rotate
Move two pointers around each other to rotate the image.
<div>
<div class="bg-image">
<img
mdbPointerGestures
[ngStyle]="{ transform: rotateTransform }"
src="https://mdbootstrap.com/img/new/standard/city/053.webp"
class="img-fluid"
id="rotate"
(rotate)="onRotate($event)"
/>
</div>
</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
rotateTransform: string;
constructor() {}
onRotate(event: any) {
this.rotateTransform = `rotate(${event.currentAngle}turn)`;
}
}
Touch - API
Import
Copy the PointerGesturesDirective from the Configuration section of the Examples
tab, then import it into the component that handles gestures.
import { Component } from '@angular/core';
import { PointerGesturesDirective } from './pointer-gestures.directive';
@Component({
selector: 'app-root',
imports: [PointerGesturesDirective],
templateUrl: './app.component.html',
})
export class AppComponent {}
Options
| Name | Type | Default | Description |
|---|---|---|---|
pressDuration |
number | 250 |
Defines how long the pointer must remain down before the directive emits press. |
doubleTapInterval |
number | 500 |
Defines the maximum delay in milliseconds between two taps. |
tapMaxDistance |
number | 10 |
Defines how far the pointer can move in pixels and still count as a tap. |
swipeThreshold |
number | 50 |
Defines the minimum swipe distance in pixels. |
swipeMaxDuration |
number | 500 |
Defines the maximum swipe duration in milliseconds. |
Outputs
| Name | Type | Description |
|---|---|---|
tap |
EventEmitter<{ x: number; y: number }> | Emits when the pointer is released without exceeding the movement limit. |
doubleTap |
EventEmitter<{ x: number; y: number }> | Emits after two taps within the configured interval. |
press |
EventEmitter<void> | Emits after the pointer remains down for the configured duration. |
panstart |
EventEmitter<void> | Emits when a one-pointer pan starts. |
panmove, panleft, panright,
panup, pandown
|
EventEmitter<PanEvent> | Emits the horizontal and vertical distance from the pointer's start position. |
pinch |
EventEmitter<{ ratio: number; origin: { x: number; y: number } }> | Emits the distance ratio and midpoint for a two-pointer pinch. |
swipeleft, swiperight, swipeup,
swipedown
|
EventEmitter<void> | Emits when a pointer movement meets the swipe distance and duration limits. |
rotate |
EventEmitter<{ currentAngle: number }> | Emits the rotation between two pointers as a fraction of one turn. |
<button
mdbPointerGestures
type="button"
class="btn btn-primary"
[pressDuration]="500"
(press)="onPress()"
>
Press and hold
</button>
import { Component } from '@angular/core';
import { PointerGesturesDirective } from './pointer-gestures.directive';
@Component({
selector: 'app-root',
imports: [PointerGesturesDirective],
templateUrl: './app.component.html',
})
export class AppComponent {
onPress(): void {
console.log('Pressed');
}
}