Custom scrolling for screen fragments

Scroll to elements inside fragments using custom swipe logic when scrollUntilVisible fails on partial screens.

Maestro’s native scrollUntilVisible is a powerful tool, but it works in a specific way:

  1. Checks for the element defined by the selector.

  2. Swipe up from the center of the screen.

  3. Return to step 1.

While this works for most full-screen lists, it fails in apps where only a specific part of the screen is scrollable, such as a bottom sheet, a small side-scrolling widget, or a fragmented UI where the top half is static. If the center of your screen isn't scrollable, the native command will simply tap a static element repeatedly.

This recipe shows you how to build a scrolling logic that swipes exactly where you need it to.

circle-info

Credits

This issue was first reported by the developers at Amicara. Their team was instrumental in helping create this solution to overcome difficulties scrolling through complex, fragmented layouts.

The workflow

To bypass the center-swipe limitation, we recreate the scrolling logic manually using a loop. Instead of letting Maestro decide where to swipe, we define specific screen coordinates to ensure the swipe happens within the scrollable container.

1. Reusable scroll Flow

This subflow (e.g., scroll_fragment.yaml) receives the TARGET_ID from the parent Flow. It continues to swipe in a restricted area of the screen until the target element is discovered.

# scroll_fragment.yaml
# Custom scrolling for targeted screen areas
# Required Env: TARGET_ID (The resource id of the element to find)

# Step 1: Initialize the state
# Use a flag to track if we've found our target yet
- evalScript: ${output.foundElement = 0}

# Step 2: Immediate check
# Before we start moving, let's see if the element is already there
- runFlow:
    when:
      visible:
        id: "${TARGET_ID}"
    commands:
      - evalScript: ${output.foundElement = 1}

# Step 3: The targeted loop
# We continue swiping in the bottom region until foundElement becomes 1
- repeat:
    while:
      true: ${output.foundElement == 0}
    commands:
      # We swipe from 90% down the screen to 75% down the screen.
      # This keeps the interaction strictly within the bottom fragment.
      - swipe:
          start: 50%, 90%
          end: 50%, 75%
      
      # Check again after the movement
      # If the element is visible, we stop the loop
      - runFlow:
          when:
            visible:
              id: "${TARGET_ID}"
          commands:
            - evalScript: ${output.foundElement = 1}

This implementation gives you full control over the scroll mechanics. In the swipe command, using percentages such as 90% and 75% ensures consistent behavior across different device sizes. By targeting the lower portion of the screen, you avoid accidentally interacting with static headers or non-scrollable hero sections. Additionally, the search flag (${output.foundElement}) acts as a bridge between the runFlow step, which checks visibility, and the repeat loop, which controls the scrolling action.

circle-check

Adapt it to your needs

circle-check

Preventing infinite swiping

2. Implementation

To use this helper in your main Flow, call it with runFlow and pass the TARGET_ID of the element you need to find as an environment variable.

Explore the following documentation pages if some of the concepts used in this recipe are not clear for you:

  • swipe: Learn how to use coordinates, durations, and percentages to move around the screen.

  • Loops: Understand the difference between looping a fixed number of times vs. looping based on a condition.

Last updated