Toast Swipe Dismiss Manipulator

Gesture manipulator that allows swiping a toast/notification to dismiss it.

Feedback feedback toast swipe dismiss gesture

Summary

ToastSwipeDismissManipulator is a PointerManipulator that adds swipe-to-dismiss gesture recognition to any VisualElement. It supports both horizontal and vertical dismiss axes. The gesture axis is locked after 12 px of movement along the dominant direction. Dismiss triggers when travel exceeds 80 px or velocity exceeds 1 100 px/s. Incomplete swipes snap back in 140 ms; dismissed elements animate out in 180 ms.

All geometry values (dismiss travel distances, offset target) are provided as delegates evaluated lazily at gesture time, so the manipulator does not need to be re-created when layout changes.

Typical use cases:

  • Swipe-to-dismiss notification toasts
  • Dismissible alert or info banners
  • Swipeable items in a list or feed

Behavior Constants

Constant Value Description
Tap slop 8 px Maximum movement before a tap becomes a drag.
Swipe axis lock threshold 12 px Movement required along the dominant axis before the axis is locked.
Swipe dismiss threshold 80 px Travel distance that triggers a dismiss.
Dismiss velocity threshold 1 100 px/s Pointer velocity that triggers a dismiss regardless of travel distance.
Snap-back duration 140 ms Animation duration for returning to the origin position.
Dismiss duration 180 ms Animation duration for the final dismiss slide-out.

USS Classes

This manipulator does not add USS classes. Visual feedback is applied by translating the getOffsetTarget element inline.

Events / Callbacks

All behavior is communicated through constructor-provided delegates.

Delegate parameter Description
Func<bool> canInteract Called on pointer-down. Return false to ignore the gesture entirely.
Func<Vector2, bool> canStartAtPosition Called with the pointer position. Return false to suppress gesture start (e.g. when a button lives in part of the toast).
Func<float> getHorizontalDismissTravelDistance Returns the horizontal distance in pixels required to trigger dismiss.
Func<float> getVerticalDismissTravelDistance Returns the vertical distance in pixels required to trigger dismiss.
Func<VisualElement> getOffsetTarget Returns the element that will be translated during the swipe. Typically the root toast element.
Action onInteractionStarted Called when the gesture crosses tap slop and is confirmed as a drag.
Action onInteractionAborted Called when a drag that was started does not meet dismiss criteria and the snap-back completes.
Action onTapped Called when the pointer is released within tap slop (no significant movement).
Action onDismissed Called after the dismiss animation completes. Dispose of the toast element here.

Public Methods

Signature Description
ResetState() Cancels any in-progress gesture and immediately restores the offset target to its origin position. Call this if the toast is hidden or replaced externally while a swipe is in progress.

Using the Control

Swipe-to-Dismiss Toast

using UnityEngine;
using UnityEngine.UIElements;
using UnityUIToolkit.Extensions;

public class ToastController : MonoBehaviour
{
    [SerializeField] private UIDocument _document;

    private VisualElement _toastRoot;
    private ToastSwipeDismissManipulator _dismissManipulator;

    private void OnEnable()
    {
        var root = _document.rootVisualElement;
        ShowToast("Session saved successfully.");
    }

    private void ShowToast(string message)
    {
        _toastRoot = new VisualElement();
        _toastRoot.AddToClassList("toast");

        var label = new Label(message);
        _toastRoot.Add(label);

        _dismissManipulator = new ToastSwipeDismissManipulator(
            canInteract: () => true,
            canStartAtPosition: _ => true,
            getHorizontalDismissTravelDistance: () => 80f,
            getVerticalDismissTravelDistance: () => 80f,
            getOffsetTarget: () => _toastRoot,
            onInteractionStarted: () => Debug.Log("Swipe started"),
            onInteractionAborted: () => Debug.Log("Swipe aborted — snapping back"),
            onTapped: () => Debug.Log("Toast tapped"),
            onDismissed: RemoveToast
        );

        _toastRoot.AddManipulator(_dismissManipulator);
        _document.rootVisualElement.Q<VisualElement>("toastLayer").Add(_toastRoot);
    }

    private void RemoveToast()
    {
        _toastRoot?.RemoveFromHierarchy();
        _toastRoot = null;
    }

    // Call this if you need to forcibly clear the toast without waiting for a gesture
    public void ForceClose()
    {
        _dismissManipulator?.ResetState();
        RemoveToast();
    }
}