diff --git a/libs/ui-lab/src/lib/components/toast/toast-stack.ts b/libs/ui-lab/src/lib/components/toast/toast-stack.ts
index 8a0aebad4..4d84d83da 100644
--- a/libs/ui-lab/src/lib/components/toast/toast-stack.ts
+++ b/libs/ui-lab/src/lib/components/toast/toast-stack.ts
@@ -29,6 +29,9 @@ import { ScToaster } from './toaster';
(animationend)="onAnimationEnd($event, toast.id)"
(pointerEnter)="toastService.pause(toast.id)"
(pointerLeave)="toastService.resume(toast.id)"
+ (touchstart)="onTouchStart($event)"
+ (touchmove)="onTouchMove($event)"
+ (touchend)="onTouchEnd($event, toast.id)"
>
@if (toast.title) {
@@ -77,6 +80,8 @@ import { ScToaster } from './toaster';
export class ScToastStack {
protected readonly toastService = inject(ScToaster);
+ private readonly swipeThreshold = 100;
+
protected dismiss(id: string): void {
this.toastService.dismiss(id);
}
@@ -95,4 +100,36 @@ export class ScToastStack {
this.toastService.remove(id);
}
}
+
+ protected onTouchStart(event: TouchEvent): void {
+ const el = event.currentTarget as HTMLElement;
+ el.dataset['swipeStartX'] = String(event.touches[0].clientX);
+ el.style.transition = 'none';
+ }
+
+ protected onTouchMove(event: TouchEvent): void {
+ const el = event.currentTarget as HTMLElement;
+ const startX = Number(el.dataset['swipeStartX'] ?? 0);
+ const deltaX = event.touches[0].clientX - startX;
+
+ el.style.transform = `translateX(${deltaX}px)`;
+ el.style.opacity = String(
+ Math.max(0, 1 - Math.abs(deltaX) / this.swipeThreshold),
+ );
+ }
+
+ protected onTouchEnd(event: TouchEvent, id: string): void {
+ const el = event.currentTarget as HTMLElement;
+ const startX = Number(el.dataset['swipeStartX'] ?? 0);
+ const deltaX = event.changedTouches[0].clientX - startX;
+
+ if (Math.abs(deltaX) >= this.swipeThreshold) {
+ this.toastService.dismiss(id);
+ } else {
+ // Snap back
+ el.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
+ el.style.transform = '';
+ el.style.opacity = '';
+ }
+ }
}