# Scroll table thru up and down automatically

**URL:** <https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194>\
**Category:** Ignition\
**Tags:** perspective\
**Created:** [September 25, 2026, 8:24pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194 "2026-09-25T20:24:16Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![Stanislau\_Yakubouski](https://avatars.discourse-cdn.com/v4/letter/s/5e9695/32.png) [@Stanislau\_Yakubouski](https://forum.inductiveautomation.com/u/Stanislau_Yakubouski)\
**Post date:** [September 25, 2026, 8:24pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/1 "2026-09-25T20:24:16Z")

</div>

Hi,

I have a remote TV displaying data in table component and I would like to scroll thru records automatically to display all the records if they overflowing. Is there a script or build-in method I can use to do that ?

---

<div class="post-metadata">

**Author:** ![djhammett77](https://avatars.discourse-cdn.com/v4/letter/d/41988e/32.png) [@djhammett77](https://forum.inductiveautomation.com/u/djhammett77)\
**Post date:** [September 25, 2026, 8:29pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/2 "2026-09-25T20:29:07Z")

</div>

One simple method would be to enable pagination on the table (configure the 'pager' section of component props) and then use a timer in an expression binding on the 'activePage' property to update which page is displayed at a pre-specified interval (assuming you know exactly how many pages will exist based on the total number of rows and the number of rows per page. If not, you could still calculate this by looking at the number of rows in your dataset at runtime and use a custom prop to store the page count for reference). This wouldn't really be "scrolling", but I'd argue that it's hard to track data in a scrolling table and I'd prefer the rows to be static and jump between pages anyway.

---

<div class="post-metadata">

**Author:** ![Ryan\_Deardorff](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/ryan_deardorff/32/63815_2.png) [@Ryan\_Deardorff](https://forum.inductiveautomation.com/u/Ryan_Deardorff)\
**Post date:** [September 25, 2026, 8:34pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/3 "2026-09-25T20:34:43Z")

</div>

I like @djhammett77's suggestion, but actual scrolling _might_ be possible by triggering a `.focus()` on an embedded view of a table cell. There was a similar question not long ago for an Accordion list, and I was able to get it working in that context:

> [@How to Implement Auto Scroll in Perspective Component](https://forum.inductiveautomation.com/t/how-to-implement-auto-scroll-in-perspective-component/114660/3):
>
> You can invoke .focus() on the newly added item in its onStartup event handler. You just need to make sure that the accordion itself first gains focus. Here’s an example view with an accordion: { "custom": {}, "params": {}, "props": { "defaultSize": { "height": 237 } }, "root": { "children": [ { "children": [ { "children": [ { "meta": { "name": "Accordion" }, …

---

<div class="post-metadata">

**Author:** ![djhammett77](https://avatars.discourse-cdn.com/v4/letter/d/41988e/32.png) [@djhammett77](https://forum.inductiveautomation.com/u/djhammett77)\
**Post date:** [September 25, 2026, 8:39pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/4 "2026-09-25T20:39:51Z")

</div>

> [@How to Implement Auto Scroll in Perspective Component](https://forum.inductiveautomation.com/t/how-to-implement-auto-scroll-in-perspective-component/114660/3):
>
> newly added item

I'd be interested to see if anyone gets this method to work in a table, given that it originally relied on an onStartup event when a new item is added to an Accordion and the data rows in a table are typically all available when the table first populates rather than being added one at a time... I'm sure it would be possible to append to the table data once piece at a time to enable something like this, but I'd be concerned that it would impact performance (churn) in obtaining the data from the source. Just rough thoughts, though

---

<div class="post-metadata">

**Author:** ![colby-noah](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/colby-noah/32/87508_2.png) [@colby-noah](https://forum.inductiveautomation.com/u/colby-noah)\
**Post date:** [September 25, 2026, 11:59pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/5 "2026-09-25T23:59:27Z")

</div>

I have a hacky use-at-your-own-risk solution which uses the Markdown component with JavaScript injection. IA has said that they plan on patching script injection, but for the time being it works. It could be worth looking into @bmusson's Embr-Periscope module which provides a way of executing JavaScript.

> [@Musson Industrial's Embr-Periscope Module](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047):
>
> Embr-Periscope An Ignition module that includes design extensions and enhancements for Perspective. A periscope provides a new perspective by allowing users to see over, around, or through obstacles, offering a unique view otherwise obstructed. NOTE: This modules is still in pre-release. All versions before v1.0.0 should be considered unstable. Components [Embedded View +](https://docs.mussonindustrial.com/ignition/embr-periscope/components/embedded-view-plus/) This component is a enhancement of the default Embedded View component with server-side property mapping. In testing, t…

To get it to work you'll have to:

1. Set your table's `meta.domId` to match the ID in the script, currently "scrolling-table" (or change the ID)
2. Set `props.virtualized` to false on the table
3. Create the Markdown component by copying and pasting the JSON below

Markdown component with script:

```json
[
  {
    "type": "ia.display.markdown",
    "version": 0,
    "props": {
      "markdown": {
        "sourcePos": true,
        "escapeHtml": false
      },
      "source": "<img src=\"x\" style=\"display:none\" onerror=\"(() => {\n const ID = '#scrolling-table';\n const PAUSE = 3000;\n const init = () => {\n const el = document.querySelector(ID);\n if (!el) return setTimeout(init, 500);\n if (el.initialized) return;\n el.initialized = true;\n el.scrollIsReset = false;\n const startScroll = () => {\n if (el.scrollInterval || el.scrollIsReset) return;\n el.scrollInterval = setInterval(() => {\n const body = el.querySelector('.flexBody');\n if (!body || el.scrollIsReset) return;\n if (body.scrollTop + body.offsetHeight >= body.scrollHeight - 2) {\n el.scrollIsReset = true;\n el.scrollTimeout = setTimeout(() => {\n body.scrollTo({ top: 0, behavior: 'smooth' });\n body.addEventListener('scrollend', function onEnd() {\n body.removeEventListener('scrollend', onEnd);\n el.scrollTimeout = setTimeout(() => { el.scrollIsReset = false; }, PAUSE);\n }, { once: true });\n }, PAUSE);\n } else {\n body.scrollTop += 1;\n }\n }, 30);\n };\n el.addEventListener('mouseover', () => { \n clearInterval(el.scrollInterval); el.scrollInterval = null;\n clearTimeout(el.scrollTimeout); el.scrollIsReset = false;\n const body = el.querySelector('.flexBody');\n if (body) { body.scrollTop = body.scrollTop; }\n }, true);\n el.addEventListener('mouseout', (e) => {\n if (!el.contains(e.relatedTarget)) startScroll();\n }, true);\n el.scrollIsReset = true;\n el.scrollTimeout = setTimeout(() => { el.scrollIsReset = false; startScroll(); }, PAUSE);\n };\n init();\n})()\">"
    },
    "meta": {
      "name": "AutoScrollTable"
    },
    "position": {
      "shrink": 0,
      "display": false
    },
    "custom": {},
    "propConfig": {
      "props.source": {
        "access": "PUBLIC"
      }
    }
  }
]

```

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [September 26, 2026, 12:57am UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/6 "2026-09-26T00:57:34Z")

</div>

> [@colby-noah](#):
>
> It could be worth looking into @bmusson's Embr-Periscope module which provides a way of executing JavaScript.

Yup, you can easily do this with Periscope. This example uses a React component to drive the scroll behavior on the Table.

[embr-testing\_2026-09-26\_0054.zip](https://forum.inductiveautomation.com/uploads/short-url/mxfpNs48bPfnSkqtRSiTzNviytR.zip) (26.9 KB)

The meat and potatoes:

```tsx
// Examples/AutoScroll

const { useEffect } = React;

type AutoScrollTableProps = {
  /**
   * The native Perspective table to scroll.
   * Either a bare DOM id (the table's meta.domId, e.g. "scrolling-table")
   * or any CSS selector (e.g. "#scrolling-table", ".my-table-class").
   */
  table: string;
  /** Pause (ms) before scrolling starts, at the bottom, and after returning to the top. */
  pause?: number;
  /** Tick rate (ms) of the scroll loop. */
  interval?: number;
  /** Pixels scrolled per tick. Fractional values are fine. */
  step?: number;
  /** Stop scrolling while the mouse is over the table. */
  pauseOnHover?: boolean;
  /** Log what the component finds to the browser console. */
  debug?: boolean;
};

const toSelector = (target: string): string =>
  /^[#.[]/.test(target) || /[\s>:~+]/.test(target) ? target : `#${CSS.escape(target)}`;

export default function AutoScrollTable({
  table,
  pause = 3000,
  interval = 30,
  step = 1,
  pauseOnHover = true,
  debug = false,
}: AutoScrollTableProps) {
  useEffect(() => {
    if (!table) return;

    const selector = toSelector(table);
    let el: HTMLElement | null = null;
    let hovered = false;
    let pos = 0; // float accumulator so small steps still move at non-100% zoom

    let tickTimer: number | undefined;
    let waitTimer: number | undefined;
    let pollTimer: number | undefined;

    const log = (...args: unknown[]) => {
      if (debug) console.log("[AutoScrollTable]", ...args);
    };

    const isScrollable = (node: HTMLElement) => {
      const overflowY = getComputedStyle(node).overflowY;
      return (
        (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") &&
        node.scrollHeight > node.clientHeight + 1
      );
    };

    // The element that actually scrolls isn't always '.flexBody' (it varies by
    // Perspective version/table config), so find the first vertically scrollable
    // descendant. Cached, and re-found if Perspective replaces it.
    let scroller: HTMLElement | null = null;
    const findScroller = (root: HTMLElement): HTMLElement | null => {
      const preferred = root.querySelector<HTMLElement>(".flexBody");
      if (preferred && isScrollable(preferred)) return preferred;
      if (isScrollable(root)) return root;
      for (const node of root.querySelectorAll<HTMLElement>("*")) {
        if (isScrollable(node)) return node;
      }
      return null;
    };
    const getBody = () => {
      if (!el) return null;
      if (scroller && scroller.isConnected && el.contains(scroller) && isScrollable(scroller)) {
        return scroller;
      }
      const next = findScroller(el);
      if (next !== scroller) {
        log(next ? "scroll container:" : "no scrollable container found (rows may fit)", next);
        if (next) log("scrollHeight", next.scrollHeight, "clientHeight", next.clientHeight);
      }
      scroller = next;
      return scroller;
    };

    const clearTimers = () => {
      window.clearInterval(tickTimer);
      window.clearTimeout(waitTimer);
      window.clearInterval(pollTimer);
      tickTimer = waitTimer = pollTimer = undefined;
    };

    const startScrolling = () => {
      clearTimers();
      if (hovered) return;
      pos = getBody()?.scrollTop ?? 0;
      tickTimer = window.setInterval(tick, interval);
    };

    const resumeAfter = (ms: number) => {
      clearTimers();
      waitTimer = window.setTimeout(startScrolling, ms);
    };

    const resetToTop = () => {
      clearTimers();
      waitTimer = window.setTimeout(() => {
        const body = getBody();
        if (!body) return startScrolling();
        body.scrollTo({ top: 0, behavior: "smooth" });

        // Poll instead of relying on 'scrollend' (not supported everywhere).
        const started = Date.now();
        pollTimer = window.setInterval(() => {
          const current = getBody();
          const timedOut = Date.now() - started > 3000;
          if (!current || current.scrollTop <= 1 || timedOut) {
            if (current && timedOut) current.scrollTop = 0;
            resumeAfter(pause);
          }
        }, 50);
      }, pause);
    };

    const tick = () => {
      const body = getBody();
      if (!body) return;
      const max = body.scrollHeight - body.clientHeight;
      if (max <= 1) return; // content fits, nothing to scroll

      // Resync if the user scrolled or the rows changed underneath us.
      if (Math.abs(body.scrollTop - pos) > 2) pos = body.scrollTop;

      if (body.scrollTop >= max - 2) {
        resetToTop();
        return;
      }
      pos = Math.min(pos + step, max);
      body.scrollTop = pos;
    };

    const onEnter = () => {
      hovered = true;
      clearTimers();
      const body = getBody();
      if (body) body.scrollTop = body.scrollTop; // halts an in-flight smooth scroll
    };

    const onLeave = () => {
      hovered = false;
      startScrolling();
    };

    const attach = (node: HTMLElement) => {
      el = node;
      log("attached to", selector, el);
      if (pauseOnHover) {
        el.addEventListener("mouseenter", onEnter);
        el.addEventListener("mouseleave", onLeave);
        hovered = el.matches(":hover");
        if (hovered) log("mouse is over the table; waiting for it to leave");
      }
      resumeAfter(pause);
    };

    const detach = () => {
      clearTimers();
      if (el) {
        el.removeEventListener("mouseenter", onEnter);
        el.removeEventListener("mouseleave", onLeave);
      }
      el = null;
      scroller = null;
      hovered = false;
    };

    // Find the table, and re-find it if Perspective remounts it.
    const watch = () => {
      if (el?.isConnected) return;
      if (el) detach();
      const found = document.querySelector<HTMLElement>(selector);
      if (found) attach(found);
      else if (!warned) {
        warned = true;
        log("no element matches", selector, "- still looking");
      }
    };
    let warned = false;

    watch();
    const watchTimer = window.setInterval(watch, 500);

    return () => {
      window.clearInterval(watchTimer);
      detach();
    };
  }, [table, pause, interval, step, pauseOnHover, debug]);

  // Behaviour-only component: renders nothing.
  return null;
}

```

@nminchin Claude authored 🙃

---

<div class="post-metadata">

**Author:** ![Aiasa21](https://avatars.discourse-cdn.com/v4/letter/a/aeb1de/32.png) [@Aiasa21](https://forum.inductiveautomation.com/u/Aiasa21)\
**Post date:** [September 26, 2026, 5:32am UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/7 "2026-09-26T05:32:54Z")

</div>

@bmusson Very interesting! It's quite annoying that when reading a table (set to 50 rows), moving to the next page leaves you at the bottom. Is it possible to intercept the page change to automatically scroll back to the top row?

---

<div class="post-metadata">

**Author:** ![nminchin](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/nminchin/32/6911_2.png) [@nminchin](https://forum.inductiveautomation.com/u/nminchin)\
**Post date:** [September 26, 2026, 6:38am UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/8 "2026-09-26T06:38:03Z")

</div>

> [@bmusson](#):
>
> Claude authored

Haha nice one! Sorry if I converted you 😂

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [September 26, 2026, 2:49pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/9 "2026-09-26T14:49:03Z")

</div>

> [@Aiasa21](#):
>
> Is it possible to intercept the page change to automatically scroll back to the top row?

Absolutely:

  
[embr-testing\_2026-09-26\_1448.zip](https://forum.inductiveautomation.com/uploads/short-url/7HmMdH0n11mGon4vKxyoXpF0pCA.zip) (50.7 KB)

```tsx
const { useEffect } = React;

type ScrollTopOnPageChangeProps = {
  /**
   * The native Perspective table to watch.
   * Either a bare DOM id (the table's meta.domId, e.g. "MyTable")
   * or any CSS selector (e.g. "#MyTable", ".my-table-class").
   */
  table: string;
  /** Animate the scroll back to the top instead of jumping. */
  smooth?: boolean;
  /** Log what the component finds to the browser console. */
  debug?: boolean;
};

const SCROLLER = ".ia_table __body .ReactVirtualized__ Grid";
const ACTIVE_PAGE = ".ia_pager .ia_pager__page--active";

const toSelector = (target: string): string =>
  /^[#.[]/.test(target) || /[\s>:~+]/.test(target) ? target : `#${CSS.escape(target)}`;

export default function ScrollTopOnPageChange({ table, smooth = false, debug = false }: ScrollTopOnPageChangeProps) {
  useEffect(() => {
    if (!table) return;

    const selector = toSelector(table);
    let el: HTMLElement | null = null;
    let observer: MutationObserver | null = null;
    let lastPage: string | null = null;
    let checkFrame: number | undefined;
    let warned = false;

    const log = (...args: unknown[]) => {
      if (debug) console.log("[ScrollTopOnPageChange]", ...args);
    };

    const scrollToTop = () => {
      // Wait two frames so the new page's rows are rendered first.
      requestAnimationFrame(() =>
        requestAnimationFrame(() => {
          const scroller = el?.querySelector<HTMLElement>(SCROLLER);
          if (!scroller) return;
          log("scrolling to top");
          scroller.scrollTo({ top: 0, behavior: smooth ? "smooth" : "auto" });
        }),
      );
    };

    // The page number shown on the highlighted pager button, or null when
    // pagination is off or the pager is mid-render.
    const check = () => {
      checkFrame = undefined;
      const page = el?.querySelector(ACTIVE_PAGE)?.textContent?.trim() || null;
      if (page === null) return;
      if (lastPage !== null && page !== lastPage) {
        log("page", lastPage, "->", page);
        scrollToTop();
      }
      lastPage = page;
    };

    // Coalesce bursts of DOM changes into one check per frame.
    const scheduleCheck = () => {
      if (checkFrame === undefined) checkFrame = requestAnimationFrame(check);
    };

    const attach = (node: HTMLElement) => {
      el = node;
      log("attached to", selector, el);
      observer = new MutationObserver(scheduleCheck);
      observer.observe(el, {
        subtree: true,
        childList: true,
        characterData: true,
        attributes: true,
        attributeFilter: ["class"],
      });
      check();
      if (lastPage === null) log("no active pager button found - is pagination enabled?");
    };

    const detach = () => {
      observer?.disconnect();
      observer = null;
      if (checkFrame !== undefined) cancelAnimationFrame(checkFrame);
      checkFrame = undefined;
      el = null;
      lastPage = null;
    };

    // Find the table, and re-find it if Perspective remounts it.
    const watch = () => {
      if (el?.isConnected) return;
      if (el) detach();
      const found = document.querySelector<HTMLElement>(selector);
      if (found) {
        warned = false;
        attach(found);
      } else if (!warned) {
        warned = true;
        log("no element matches", selector, "- still looking");
      }
    };

    watch();
    const watchTimer = window.setInterval(watch, 500);

    return () => {
      window.clearInterval(watchTimer);
      detach();
    };
  }, [table, smooth, debug]);

  // Behaviour-only component: renders nothing.
  return null;
}

```

---

<div class="post-metadata">

**Author:** ![Aiasa21](https://avatars.discourse-cdn.com/v4/letter/a/aeb1de/32.png) [@Aiasa21](https://forum.inductiveautomation.com/u/Aiasa21)\
**Post date:** [September 26, 2026, 4:11pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/10 "2026-09-26T16:11:50Z")

</div>

Thanks for the response!

I definitely need to dive deeper into these aspects because I think the possibilities are incredible, and the only real limit is our imagination.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [September 26, 2026, 4:44pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/11 "2026-09-26T16:44:21Z")

</div>

This version:

1. Combines all of this into one `TableHelper`
2. Adds new scroll modes (`row`, `screen`, and `continuous`)
3. Adds an `allPages` feature. Enabling this will cause the auto-scroll to advance between pages. This is turned off in the Designer because it messes with editting focus.
4. Adds a `publishState` feature that causes the current scroll state to be written to `props.state`.

[embr-periscope-table-helper.zip](https://forum.inductiveautomation.com/uploads/short-url/7kLxQy5J9Rpo5ZDdRn3PIcmtDcH.zip) (69.4 KB)

> [@Aiasa21](#):
>
> I definitely need to dive deeper into these aspects because I think the possibilities are incredible, and the only real limit is our imagination.

If you've got ideas, post them in the Periscope thread and I'd be happy to explore them too.  
I've come back from ICC with a bunch of interesting ideas 😆

---

<div class="post-metadata">

**Author:** ![Stanislau\_Yakubouski](https://avatars.discourse-cdn.com/v4/letter/s/5e9695/32.png) [@Stanislau\_Yakubouski](https://forum.inductiveautomation.com/u/Stanislau_Yakubouski)\
**Post date:** [September 26, 2026, 10:52pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/12 "2026-09-26T22:52:45Z")

</div>

That looks exactly what I was looking for I will try that. Appreciate sharing it

---

<div class="post-metadata">

**Author:** ![Stanislau\_Yakubouski](https://avatars.discourse-cdn.com/v4/letter/s/5e9695/32.png) [@Stanislau\_Yakubouski](https://forum.inductiveautomation.com/u/Stanislau_Yakubouski)\
**Post date:** [September 26, 2026, 10:53pm UTC](https://forum.inductiveautomation.com/t/scroll-table-thru-up-and-down-automatically/117194/13 "2026-09-26T22:53:57Z")

</div>

shuffling using pager is very smart
