Skip to main content
Glama

scroll_by

Scroll any container or page by precise pixel amounts to test infinite scroll, sticky headers, or carousel navigation. Auto-detects scroll direction when only one axis is available.

Instructions

Scroll a container (or page) by a specific number of pixels. Auto-detects scroll direction when only one is available. Essential for: testing sticky headers/footers, triggering infinite scroll, carousel navigation, precise scroll position testing. Use 'html' or 'body' for page scrolling. Positive pixels = down/right, negative = up/left. Outputs: ✓ success summary with axis position and percent of max scroll; ⚠️ boundary notice when movement is limited; ⚠️ ambiguous-direction guidance when both axes scroll; ⚠️ not-scrollable report with ancestor suggestions; 💡 follow-up tips matching the detected scenario.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
selectorYesCSS selector of scrollable container (use 'html' or 'body' for page scroll, e.g., 'testid:chat-container', '.scrollable-list', 'html')
pixelsYesNumber of pixels to scroll. Positive = down/right, negative = up/left. Example: 500, -200
directionNoScroll direction: 'vertical' (default), 'horizontal', or 'auto' (detects available direction). Use 'auto' for smart detection.

Implementation Reference

  • The execute() method of ScrollByTool class (extends BrowserToolBase). Handles page scrolling and element scrolling via Playwright's page.evaluate, with auto-direction detection, boundary checking, and rich response messages.
    async execute(args: any, context: ToolContext): Promise<ToolResponse> {
      this.recordInteraction();
      return this.safeExecute(context, async (page) => {
        const selector = this.normalizeSelector(args.selector);
        const pixels = args.pixels;
        const direction = args.direction || 'vertical'; // Default to vertical for backward compatibility
    
        // Check if scrolling the page (html/body) or a specific element
        const isPageScroll = selector === 'html' || selector === 'body';
    
        if (isPageScroll) {
          // Scroll the page
          const scrollResult = await page.evaluate(({ scrollAmount, scrollDirection }) => {
            const maxVertical = document.documentElement.scrollHeight - window.innerHeight;
            const maxHorizontal = document.documentElement.scrollWidth - window.innerWidth;
    
            // Auto-detect direction if needed
            let actualDirection = scrollDirection;
            if (scrollDirection === 'auto') {
              const verticalScrollable = maxVertical > 0;
              const horizontalScrollable = maxHorizontal > 0;
    
              if (verticalScrollable && !horizontalScrollable) {
                actualDirection = 'vertical';
              } else if (horizontalScrollable && !verticalScrollable) {
                actualDirection = 'horizontal';
              } else if (verticalScrollable && horizontalScrollable) {
                // Both directions scrollable - need explicit direction
                return {
                  error: 'ambiguous',
                  maxVertical,
                  maxHorizontal
                };
              } else {
                // Neither direction scrollable
                return {
                  error: 'not-scrollable',
                  maxVertical: 0,
                  maxHorizontal: 0
                };
              }
            }
    
            // Perform the scroll
            const isVertical = actualDirection === 'vertical';
            const previousScroll = isVertical ? window.scrollY : window.scrollX;
    
            if (isVertical) {
              window.scrollBy(0, scrollAmount);
            } else {
              window.scrollBy(scrollAmount, 0);
            }
    
            const newScroll = isVertical ? window.scrollY : window.scrollX;
            const actualScrolled = newScroll - previousScroll;
    
            return {
              previous: previousScroll,
              new: newScroll,
              actualScrolled,
              maxScroll: isVertical ? maxVertical : maxHorizontal,
              direction: actualDirection,
              maxVertical,
              maxHorizontal
            };
          }, { scrollAmount: pixels, scrollDirection: direction });
    
          // Handle error cases
          if ('error' in scrollResult) {
            if (scrollResult.error === 'ambiguous') {
              return createSuccessResponse([
                `⚠️  Page is scrollable in both directions`,
                `Vertical: ${scrollResult.maxVertical}px max scroll`,
                `Horizontal: ${scrollResult.maxHorizontal}px max scroll`,
                ``,
                `💡 Specify direction explicitly:`,
                `   scroll_by({ selector: "html", pixels: ${pixels}, direction: "vertical" })`,
                `   scroll_by({ selector: "html", pixels: ${pixels}, direction: "horizontal" })`
              ]);
            } else if (scrollResult.error === 'not-scrollable') {
              return createSuccessResponse([
                `⚠️  Page is not scrollable in any direction`,
                `Content fits within viewport (no overflow)`,
                `Position: unchanged`
              ]);
            }
          }
    
          const isVertical = scrollResult.direction === 'vertical';
          const directionWord = isVertical
            ? (pixels > 0 ? 'down' : 'up')
            : (pixels > 0 ? 'right' : 'left');
          const axis = isVertical ? 'y' : 'x';
    
          const messages = [
            `✓ Scrolled page ${directionWord} ${Math.abs(pixels)}px (${scrollResult.direction})`,
            `Position: ${axis}=${scrollResult.new}px (was ${scrollResult.previous}px)`
          ];
    
          // Add info if we hit the scroll boundary
          const hitBoundary = Math.abs(scrollResult.actualScrolled) < Math.abs(pixels);
          if (hitBoundary) {
            const boundary = pixels > 0 ? (isVertical ? 'bottom' : 'right') : (isVertical ? 'top' : 'left');
            messages.push(`⚠️  Reached ${boundary} of page (max scroll: ${scrollResult.maxScroll}px)`);
          }
    
          // Contextual suggestions
          if (isVertical && pixels > 0 && !hitBoundary) {
            // Scrolling down on page - suggest sticky header testing
            messages.push('');
            messages.push('💡 Common next step - Test sticky header/footer:');
            messages.push('   measure_element({ selector: "header" }) - Check if position stays fixed');
          } else if (hitBoundary && pixels > 0) {
            // Hit boundary - suggest checking for infinite scroll or lazy-loaded content
            messages.push('');
            messages.push('💡 At page boundary - Check for dynamic content:');
            messages.push('   element_visibility({ selector: "..." }) - Verify lazy-loaded elements appeared');
            messages.push('   inspect_dom() - See if new content was added');
          }
    
          return createSuccessResponse(messages);
        } else {
          // Scroll a specific element
          const locator = await this.createScopedLocator(page, args.selector);
    
          // Check if element exists
          const count = await locator.count();
          if (count === 0) {
            return createSuccessResponse([
              `✗ Element not found: ${args.selector}`,
              ``,
              `💡 Try:`,
              `   • Use 'html' or 'body' to scroll the page`,
              `   • Use inspect_dom() to find scrollable containers`,
              `   • Use get_test_ids() to see available test IDs`
            ]);
          }
    
          // Use standard element selection with error on multiple matches
          const { element } = await this.selectPreferredLocator(locator, {
            errorOnMultiple: true,
            originalSelector: args.selector,
          });
    
          // Scroll the element and collect scrollable ancestor info
          const scrollResult = await element.evaluate(
            (el, { scrollAmount, scrollDirection }) => {
              const maxVertical = el.scrollHeight - el.clientHeight;
              const maxHorizontal = el.scrollWidth - el.clientWidth;
    
              const getClassName = (element: Element) => {
                const value = (element as any).className as unknown;
                if (typeof value === 'string') {
                  return value;
                }
                if (
                  value &&
                  typeof (value as { baseVal?: unknown }).baseVal === 'string'
                ) {
                  return (value as { baseVal: string }).baseVal;
                }
                return '';
              };
    
              // Auto-detect direction if needed
              let actualDirection = scrollDirection;
              if (scrollDirection === 'auto') {
                const verticalScrollable = maxVertical > 0;
                const horizontalScrollable = maxHorizontal > 0;
    
                if (verticalScrollable && !horizontalScrollable) {
                  actualDirection = 'vertical';
                } else if (horizontalScrollable && !verticalScrollable) {
                  actualDirection = 'horizontal';
                } else if (verticalScrollable && horizontalScrollable) {
                  // Both directions scrollable - need explicit direction
                  return {
                    error: 'ambiguous',
                    maxVertical,
                    maxHorizontal,
                    tagName: el.tagName ? el.tagName.toLowerCase() : 'element',
                    testId: el.getAttribute('data-testid'),
                    id: (el as any).id ?? null,
                    className: getClassName(el)
                  };
                } else {
                  // Neither direction scrollable - collect ancestors
                  const scrollableAncestors: Array<{
                    tagName: string;
                    testId: string | null;
                    id: string | null;
                    className: string;
                    maxScrollVertical: number;
                    maxScrollHorizontal: number;
                  }> = [];
    
                  let parent = el.parentElement;
                  while (parent && scrollableAncestors.length < 3) {
                    const maxParentVertical =
                      parent.scrollHeight - parent.clientHeight;
                    const maxParentHorizontal =
                      parent.scrollWidth - parent.clientWidth;
                    if (maxParentVertical > 0 || maxParentHorizontal > 0) {
                      scrollableAncestors.push({
                        tagName: parent.tagName
                          ? parent.tagName.toLowerCase()
                          : 'element',
                        testId: parent.getAttribute('data-testid'),
                        id: (parent as any).id ?? null,
                        className: getClassName(parent),
                        maxScrollVertical: maxParentVertical,
                        maxScrollHorizontal: maxParentHorizontal
                      });
                    }
                    parent = parent.parentElement;
                  }
    
                  return {
                    error: 'not-scrollable',
                    maxVertical: 0,
                    maxHorizontal: 0,
                    tagName: el.tagName ? el.tagName.toLowerCase() : 'element',
                    testId: el.getAttribute('data-testid'),
                    id: (el as any).id ?? null,
                    className: getClassName(el),
                    scrollableAncestors
                  };
                }
              }
    
              // Perform the scroll
              const isVertical = actualDirection === 'vertical';
              const previousScroll = isVertical ? el.scrollTop : el.scrollLeft;
    
              if (isVertical) {
                el.scrollTop += scrollAmount;
              } else {
                el.scrollLeft += scrollAmount;
              }
    
              const newScroll = isVertical ? el.scrollTop : el.scrollLeft;
              const actualScrolled = newScroll - previousScroll;
    
              // Find scrollable ancestors (up to 3)
              const scrollableAncestors: Array<{
                tagName: string;
                testId: string | null;
                id: string | null;
                className: string;
                maxScrollVertical: number;
                maxScrollHorizontal: number;
              }> = [];
    
              let parent = el.parentElement;
              while (parent && scrollableAncestors.length < 3) {
                const maxParentVertical =
                  parent.scrollHeight - parent.clientHeight;
                const maxParentHorizontal =
                  parent.scrollWidth - parent.clientWidth;
                if (maxParentVertical > 0 || maxParentHorizontal > 0) {
                  scrollableAncestors.push({
                    tagName: parent.tagName
                      ? parent.tagName.toLowerCase()
                      : 'element',
                    testId: parent.getAttribute('data-testid'),
                    id: (parent as any).id ?? null,
                    className: getClassName(parent),
                    maxScrollVertical: maxParentVertical,
                    maxScrollHorizontal: maxParentHorizontal
                  });
                }
                parent = parent.parentElement;
              }
    
              return {
                previous: previousScroll,
                new: newScroll,
                actualScrolled,
                maxScroll: isVertical ? maxVertical : maxHorizontal,
                direction: actualDirection,
                maxVertical,
                maxHorizontal,
                tagName: el.tagName ? el.tagName.toLowerCase() : 'element',
                testId: el.getAttribute('data-testid'),
                id: (el as any).id ?? null,
                className: getClassName(el),
                scrollableAncestors
              };
            },
            { scrollAmount: pixels, scrollDirection: direction }
          );
    
          // Build element description
          let elementDesc = `<${scrollResult.tagName}`;
          if (scrollResult.testId) elementDesc += ` data-testid="${scrollResult.testId}"`;
          else if (scrollResult.id) elementDesc += ` id="${scrollResult.id}"`;
          else if (scrollResult.className) {
            const classes = scrollResult.className.split(' ').slice(0, 2).join(' ');
            if (classes) elementDesc += ` class="${classes}"`;
          }
          elementDesc += '>';
    
          // Handle error cases
          if ('error' in scrollResult) {
            if (scrollResult.error === 'ambiguous') {
              return createSuccessResponse([
                `⚠️  ${elementDesc} is scrollable in both directions`,
                `Vertical: ${scrollResult.maxVertical}px max scroll`,
                `Horizontal: ${scrollResult.maxHorizontal}px max scroll`,
                ``,
                `💡 Specify direction explicitly:`,
                `   scroll_by({ selector: "${args.selector}", pixels: ${pixels}, direction: "vertical" })`,
                `   scroll_by({ selector: "${args.selector}", pixels: ${pixels}, direction: "horizontal" })`
              ]);
            } else if (scrollResult.error === 'not-scrollable') {
              const messages = [
                `⚠️  Container is not scrollable in any direction`,
                `${elementDesc} has max scroll: 0px vertical, 0px horizontal`,
                `Position: unchanged`
              ];
    
              // Suggest scrollable ancestors if found
              if (scrollResult.scrollableAncestors.length > 0) {
                messages.push('');
                messages.push('💡 Try these scrollable ancestors:');
                scrollResult.scrollableAncestors.forEach((ancestor, i) => {
                  // Build selector suggestion (prefer testid > id > class)
                  let suggestion = '';
                  if (ancestor.testId) {
                    suggestion = `testid:${ancestor.testId}`;
                  } else if (ancestor.id) {
                    suggestion = `#${ancestor.id}`;
                  } else if (ancestor.className) {
                    const firstClass = ancestor.className.split(' ')[0];
                    suggestion = `.${firstClass}`;
                  } else {
                    suggestion = ancestor.tagName;
                  }
    
                  const directions = [];
                  if (ancestor.maxScrollVertical > 0) directions.push(`↕️ ${ancestor.maxScrollVertical}px vertical`);
                  if (ancestor.maxScrollHorizontal > 0) directions.push(`↔️ ${ancestor.maxScrollHorizontal}px horizontal`);
    
                  messages.push(`   ${i + 1}. ${suggestion} (${directions.join(', ')})`);
                });
              } else {
                messages.push('');
                messages.push('💡 Suggestions:');
                messages.push(`   • Use 'html' or 'body' to scroll the page`);
                messages.push(`   • Use inspect_dom() to find scrollable containers`);
              }
    
              return createSuccessResponse(messages);
            }
          }
    
          // Calculate percentage of max scroll
          const percentage = scrollResult.maxScroll > 0
            ? Math.round((scrollResult.new / scrollResult.maxScroll) * 100)
            : 0;
    
          const isVertical = scrollResult.direction === 'vertical';
          const directionWord = isVertical
            ? (pixels > 0 ? 'down' : 'up')
            : (pixels > 0 ? 'right' : 'left');
          const axis = isVertical ? 'y' : 'x';
    
          const messages = [
            `✓ Scrolled ${elementDesc} ${directionWord} ${Math.abs(pixels)}px (${scrollResult.direction})`,
            `Position: ${axis}=${scrollResult.new}px (was ${scrollResult.previous}px) [${percentage}% of max: ${scrollResult.maxScroll}px]`
          ];
    
          // Add info if we hit the scroll boundary
          const hitBoundary = Math.abs(scrollResult.actualScrolled) < Math.abs(pixels);
          if (hitBoundary) {
            const boundary = pixels > 0 ? (isVertical ? 'bottom' : 'right') : (isVertical ? 'top' : 'left');
            messages.push(`⚠️  Reached ${boundary} of container`);
    
            // Suggest checking for lazy-loaded content at container boundary
            if (pixels > 0) {
              messages.push('');
              messages.push('💡 At container boundary - Check for lazy-loaded content:');
              messages.push(`   inspect_dom({ selector: "${args.selector}" }) - See if new children appeared`);
            }
          }
    
          return createSuccessResponse(messages);
        }
      });
    }
  • getMetadata() defining the tool schema: name 'scroll_by', description, inputSchema with required 'selector' (string), 'pixels' (number), and optional 'direction' enum (vertical/horizontal/auto). Uses ANNOTATIONS.interaction.
    static getMetadata(sessionConfig?: SessionConfig): ToolMetadata {
      return {
        name: "scroll_by",
        description: "Scroll a container (or page) by a specific number of pixels. Auto-detects scroll direction when only one is available. Essential for: testing sticky headers/footers, triggering infinite scroll, carousel navigation, precise scroll position testing. Use 'html' or 'body' for page scrolling. Positive pixels = down/right, negative = up/left. Outputs: ✓ success summary with axis position and percent of max scroll; ⚠️ boundary notice when movement is limited; ⚠️ ambiguous-direction guidance when both axes scroll; ⚠️ not-scrollable report with ancestor suggestions; 💡 follow-up tips matching the detected scenario.",
        annotations: ANNOTATIONS.interaction,
        inputSchema: {
          type: "object",
          properties: {
            selector: {
              type: "string",
              description: "CSS selector of scrollable container (use 'html' or 'body' for page scroll, e.g., 'testid:chat-container', '.scrollable-list', 'html')"
            },
            pixels: {
              type: "number",
              description: "Number of pixels to scroll. Positive = down/right, negative = up/left. Example: 500, -200"
            },
            direction: {
              type: "string",
              description: "Scroll direction: 'vertical' (default), 'horizontal', or 'auto' (detects available direction). Use 'auto' for smart detection.",
              enum: ["vertical", "horizontal", "auto"]
            }
          },
          required: ["selector", "pixels"],
        },
      };
    }
  • ScrollByTool is registered in the BROWSER_TOOL_CLASSES array at line 58, which gets registered via registry.ts line 65.
    export const BROWSER_TOOL_CLASSES: ToolClass[] = [
      // Navigation (5)
      NavigateTool,
      GoHistoryTool,
      ScrollToElementTool,
      ScrollByTool,
    
      // Lifecycle (2)
      CloseTool,
      SetColorSchemeTool,
    
      // Interaction (7)
      ClickTool,
      FillTool,
      SelectTool,
      HoverTool,
      UploadFileTool,
      DragTool,
      PressKeyTool,
    
      // Content (3)
      ScreenshotTool,
      GetTextTool,
      GetHtmlTool,
    
      // Inspection (10)
      InspectDomTool,
      GetTestIdsTool,
      QuerySelectorTool,
      FindByTextTool,
      CheckVisibilityTool,
      CompareElementAlignmentTool,
      InspectAncestorsTool,
      ElementExistsTool,
      MeasureElementTool,
      GetComputedStylesTool,
    
      // Evaluation (1)
      EvaluateTool,
    
      // Console (2)
      GetConsoleLogsTool,
      ClearConsoleLogsTool,
    
      // Network (2)
      ListNetworkRequestsTool,
      GetRequestDetailsTool,
    
      // Waiting (2)
      WaitForElementTool,
      WaitForNetworkIdleTool,
    ];
  • Re-exports ScrollByTool from scroll_by.ts for clean module imports.
    export { ScrollByTool } from './scroll_by.js';
  • Suggestion helper in evaluate tool that recommends scroll_by when user scripts contain scroll-related JS patterns.
    if (scriptLower.match(/scrollto|scrollby|scrollintoview|scrolltop|scrollleft|window\.scroll|pageyoffset|scrolly/)) {
      suggestions.push({ key: 'scroll', line: '📜 scroll_to_element / scroll_by — vs scrollTo/scrollIntoView/pageYOffset' });
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations indicate a mutating action (readOnlyHint=false, idempotentHint=false). The description adds valuable behavioral context: output scenarios (success, boundary, ambiguous-direction, not-scrollable), direction semantics, and auto-detection. It goes beyond annotations to explain what the agent can expect.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Every sentence adds value with no redundancy. The description uses bullet points for outputs and essential uses, making it scannable. It is slightly longer but efficiently packed with information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite no output schema, the description thoroughly covers return behavior (success, boundary, ambiguous-direction, not-scrollable reports) and usage guidance. All three parameters are addressed, and edge cases like scroll limits are hinted at via boundary notices.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, but the description adds meaning beyond the schema: contextual details on direction auto-detection, positive/negative pixel meaning, and 'html'/'body' usage. This enriches parameter understanding beyond what the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool scrolls a container by pixels, specifies auto-detection of direction, and distinguishes use cases like 'precise scroll position testing' from likely sibling 'scroll_to_element'. The verb+resource is specific and the scope is well-defined.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly lists essential use cases (testing sticky headers, infinite scroll, carousel navigation) and provides guidance for page scrolling with 'html'/'body'. However, it does not explicitly name sibling tools or state when not to use this tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/antonzherdev/mcp-web-inspector'

If you have feedback or need assistance with the MCP directory API, please join our Discord server