Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.metabind.ai/llms.txt

Use this file to discover all available pages before exploring further.

Gesture modifiers attach interaction handlers to any component. onTapGesture runs on a single (or multi-) tap; onDragGesture reports drag translation and velocity through every phase; onLongPressGesture fires after a press-and-hold; onHover tracks pointer enter/leave on web and desktop. For interactive controls, prefer Button directly — gesture modifiers are best for adding handling to otherwise non-interactive components like Text, Image, and shapes.

onTapGesture

Runs an action when the component is tapped.
.onTapGesture(action: (locationInView: Point) => void): Component
.onTapGesture(props: { count: number }, action: (locationInView: Point) => void): Component
action
(locationInView: Point) => void
required
A callback that receives the tap location as a Point ({ x, y }) in the component’s coordinate space.
props
object
Configuration options for the tap gesture.
Single tap
Text("Tap me")
    .onTapGesture((location) => {
        console.log("Tapped at: " + location.x + ", " + location.y)
    })
Simple tap action (ignoring location)
const body = () => {
    const [count, setCount] = useState(0)

    return Text("Tapped " + count + " times")
        .padding(16)
        .background(Color("blue").opacity(0.1))
        .cornerRadius(8)
        .onTapGesture(() => setCount(count + 1))
}
Double tap
const body = () => {
    const [zoomed, setZoomed] = useState(false)

    return Image({ url: "photo.jpg" })
        .resizable()
        .scaledToFit()
        .scaleEffect(zoomed ? 2 : 1)
        .onTapGesture({ count: 2 }, () => setZoomed(!zoomed))
}
Combining single and double tap
const body = () => {
    const [label, setLabel] = useState("Tap or double tap")

    return Text(label)
        .padding(16)
        .onTapGesture({ count: 2 }, () => setLabel("Double tapped!"))
        .onTapGesture(() => setLabel("Single tapped!"))
}
The locationInView is in the tapped component’s local coordinate space, with { x: 0, y: 0 } at the top-leading corner. When combining single and multi-tap gestures, apply the higher count gesture first — the system waits briefly to distinguish between single and multi-tap. For interactive controls like buttons, prefer using Button directly.

onDragGesture

Tracks drag gestures with translation and velocity.
.onDragGesture(action: (state: DragGestureState) => void): Component
.onDragGesture(props: { minimumDistance?: number }, action: (state: DragGestureState) => void): Component
action
(state: DragGestureState) => void
required
A callback that receives the drag gesture state throughout the gesture lifecycle.
props
object
Configuration options for the drag gesture.
Basic drag tracking
const body = () => {
    const [offset, setOffset] = useState({ x: 0, y: 0 })

    return Circle()
        .frame({ width: 60, height: 60 })
        .foregroundStyle(Color("blue"))
        .offset(offset)
        .onDragGesture((state) => {
            if (state.phase === "changed") {
                setOffset({
                    x: state.translation.x,
                    y: state.translation.y
                })
            }
            if (state.phase === "ended") {
                setOffset({ x: 0, y: 0 })
            }
        })
}
With minimum distance Require a longer drag before recognition to avoid interfering with taps.
const body = () => {
    const [dragging, setDragging] = useState(false)

    return Rectangle()
        .frame({ width: 100, height: 100 })
        .foregroundStyle(dragging ? Color("red") : Color("blue"))
        .onDragGesture({ minimumDistance: 20 }, (state) => {
            setDragging(
                state.phase === "began" ||
                state.phase === "changed"
            )
        })
}
Swipe detection using velocity
const body = () => {
    const [direction, setDirection] = useState("")

    return Text(direction || "Swipe me")
        .frame({ width: 200, height: 200 })
        .background(Color("gray").opacity(0.2))
        .onDragGesture((state) => {
            if (state.phase === "ended") {
                if (state.velocity.x > 500) {
                    setDirection("Swiped right")
                } else if (state.velocity.x < -500) {
                    setDirection("Swiped left")
                }
            }
        })
}
The gesture callback fires for every phase: "possible", "began", "changed", "ended", and "cancelled". translation is cumulative from the initial touch point, not a per-frame delta. velocity is in points per second and is useful for flick/swipe detection.

onLongPressGesture

Tracks long press gestures.
.onLongPressGesture(action: (state: GestureState) => void): Component
.onLongPressGesture(props: { minimumDuration?: number; maximumDistance?: number }, action: (state: GestureState) => void): Component
action
(state: GestureState) => void
required
A callback that receives the gesture state throughout the gesture lifecycle.
props
object
Configuration options for the long press gesture.
Basic long press
const body = () => {
    const [pressed, setPressed] = useState(false)

    return Text(pressed ? "Long pressed!" : "Press and hold")
        .padding(16)
        .background(Color("gray").opacity(0.2))
        .cornerRadius(8)
        .onLongPressGesture((state) => {
            if (state.phase === "ended") {
                setPressed(true)
            }
        })
}
Custom duration Require a 2-second press.
Text("Hold for 2 seconds")
    .padding(16)
    .onLongPressGesture({ minimumDuration: 2 }, (state) => {
        if (state.phase === "ended") {
            console.log("Long press recognized")
        }
    })
Visual feedback during press
const body = () => {
    const [pressing, setPressing] = useState(false)

    return Circle()
        .frame({ width: 80, height: 80 })
        .foregroundStyle(pressing ? Color("red") : Color("blue"))
        .scaleEffect(pressing ? 0.9 : 1)
        .onLongPressGesture((state) => {
            setPressing(
                state.phase === "began" ||
                state.phase === "changed"
            )
        })
}
The gesture callback fires for every phase: "possible", "began", "changed", "ended", and "cancelled". Use "began" and "changed" to show press-in-progress feedback, and "ended" to perform the action. If the finger moves beyond maximumDistance, the gesture is cancelled.

onHover

Fires when the pointer hovers over or leaves the component.
.onHover(action: (isHovering: boolean) => void): Component
action
(isHovering: boolean) => void
required
A callback that receives true when the pointer enters the component and false when it leaves.
Hover highlight
const body = () => {
    const [hovered, setHovered] = useState(false)

    return Text("Hover me")
        .padding(16)
        .background(hovered ? Color("blue").opacity(0.1) : Color("clear"))
        .cornerRadius(8)
        .onHover((isHovering) => setHovered(isHovering))
}
Changing text on hover
const body = () => {
    const [hovered, setHovered] = useState(false)

    return Text(hovered ? "Hovered!" : "Hover me")
        .foregroundStyle(hovered ? Color("blue") : Color("gray"))
        .onHover((isHovering) => setHovered(isHovering))
}
Scale effect on hover
const body = () => {
    const [hovered, setHovered] = useState(false)

    return Image({ url: "thumbnail.jpg" })
        .resizable()
        .frame({ width: 100, height: 100 })
        .scaleEffect(hovered ? 1.05 : 1)
        .onHover((isHovering) => setHovered(isHovering))
}
This modifier is primarily useful for web and desktop pointer interactions. On touch-only devices, hover events do not occur. Currently only supported on the web platform.

See also

  • Button — interactive control with built-in tap handling
  • contextMenu — show a menu on long press or right-click
  • allowsHitTesting — control whether a component receives events
  • contentShape — define the hit-testing region