Getting StartedComponentsDesign TokensThemingTheme SynthesizerFrameworksAccessibilityUtilitiesServer RenderingBrowser SupportContributingChangelog App ShellAspect GridAuth ShellCenterClusterContainerDashboard GridDockFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbar Anchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkSpeed DialStepper NavTabsTop BarTree View AccordionAspect RatioAvatarAvatar GroupCalloutCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSeparatorSkeletonSpinnerStackVideoVirtual List Activity HeatmapAnimated NumberBadgeChartClockComparisonCountdown TimerData GridData TableDescription ListDiffEvent CalendarGaugeJSON TreeKanbanKey ValueLevel MeterListMeterSparklineStatStepperTableTagTimelineUptimeValue CardWaveform BlockquoteCode BlockGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriter ButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputOTP InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree Select AlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuGuided TourHover CardInline MessageLoading OverlayModalNotification PanelPopoverProgressProgress ToastSheetSnackbarSpotlightToastTooltip
ARC UI ARC Radiant Components
v3.2 Docs Components Tokens Synthesizer
Getting StartedFrameworksServer Rendering Design TokensThemingTheme SynthesizerTypographyUtilities All ComponentsAccessibilityBrowser SupportChangelogContributingStats App ShellAspect GridAuth ShellCenterClusterContainerDashboard GridDockFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbar Anchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkSpeed DialStepper NavTabsTop BarTree View AccordionAspect RatioAvatarAvatar GroupCalloutCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSeparatorSkeletonSpinnerStackVideoVirtual List Activity HeatmapAnimated NumberBadgeChartClockComparisonCountdown TimerData GridData TableDescription ListDiffEvent CalendarGaugeJSON TreeKanbanKey ValueLevel MeterListMeterSparklineStatStepperTableTagTimelineUptimeValue CardWaveform BlockquoteCode BlockGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriter ButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputOTP InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree Select AlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuGuided TourHover CardInline MessageLoading OverlayModalNotification PanelPopoverProgressProgress ToastSheetSnackbarSpotlightToastTooltip

Infinite Scroll

Intersection Observer-powered container that fires a load event when the user scrolls near the bottom, with built-in loading spinner and end-of-list state.

Components Infinite Scroll
content interactive
<arc-infinite-scroll>

Overview

InfiniteScroll wraps your content in a feed container and watches for when the user approaches the bottom using an Intersection Observer sentinel element. When the sentinel enters the viewport (plus a configurable `threshold` margin), the component fires an `arc-load-more` event, signalling your application to fetch and append the next batch of items. This eliminates the need for manual scroll listeners or pagination button clicks. While data is being fetched, set the `loading` prop to `true` to display a centered spinner in the footer area. The component automatically suppresses additional `arc-load-more` events while loading is active, preventing duplicate fetch requests. Once your data arrives, reset `loading` to `false` and the sentinel resumes observation. When all data has been loaded, set the `finished` prop to `true`. The sentinel is removed and replaced with a "No more items" text indicator, and the Intersection Observer is disconnected. The container uses `aria-busy` to communicate loading state to assistive technologies. If your items are self-contained entries (e.g. posts), slot them as `<article>` elements and add `role="feed"` to the component to opt into the ARIA feed pattern.

Guidelines

When to use

  • Set `loading` to `true` immediately when you begin fetching data, and reset it to `false` when data arrives
  • Set `finished` to `true` when the API indicates no more pages remain, so the observer disconnects
  • Use a reasonable `threshold` (100-300px) so content loads before users hit the bottom and see a gap
  • Place InfiniteScroll inside a scrollable parent or let the page itself be the scroll container
  • Append new items as direct children of the component — they render in the default slot

When not to use

  • Do not leave `loading` as `true` indefinitely — this blocks further load events and leaves the spinner visible
  • Do not use InfiniteScroll for small, fixed datasets — standard pagination or a simple list is more appropriate
  • Do not set `threshold` to 0 — the user will see a flash of empty space before content loads
  • Do not nest multiple InfiniteScroll components — their observers may conflict
  • Avoid using InfiniteScroll without providing a way to reach footer content, since it pushes the footer down continuously

Features

  • Intersection Observer-based sentinel that fires `arc-load-more` when the user nears the content bottom
  • Configurable `threshold` prop (in pixels) to control how far in advance the load event triggers
  • Built-in `arc-spinner` displayed in the footer when `loading` is `true`
  • Automatic suppression of duplicate load events while loading is in progress
  • End-of-list state via `finished` prop — removes the sentinel and shows "No more items" text
  • ARIA `aria-busy` for accessible loading communication
  • Observer automatically disconnects and reconnects when `finished` or `disabled` change
  • Disabled state at 40% opacity with pointer events blocked

Preview

Usage

This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.

<arc-infinite-scroll id="feed" threshold="200">
  <!-- Items appended here -->
</arc-infinite-scroll>

<script>
  const feed = document.getElementById('feed');
  let page = 1;

  feed.addEventListener('arc-load-more', async () => {
    feed.loading = true;
    const items = await fetchPage(page++);
    items.forEach(item => feed.appendChild(createItemEl(item)));
    feed.loading = false;
    if (!items.length) feed.finished = true;
  });
</script>
import { InfiniteScroll } from '@arclux/arc-ui-react';

const [items, setItems] = useState(initialItems);
const [loading, setLoading] = useState(false);
const [finished, setFinished] = useState(false);

async function loadMore() {
  setLoading(true);
  const next = await fetchNextPage();
  setItems(prev => [...prev, ...next]);
  setLoading(false);
  if (!next.length) setFinished(true);
}

<InfiniteScroll loading={loading} finished={finished} onArcLoadMore={loadMore}>
  {items.map(item => <div key={item.id}>{item.name}</div>)}
</InfiniteScroll>
<script setup>
import { ref } from 'vue';
import { InfiniteScroll } from '@arclux/arc-ui-vue';

const items = ref(initialItems);
const loading = ref(false);
const finished = ref(false);

async function loadMore() {
  loading.value = true;
  const next = await fetchNextPage();
  items.value.push(...next);
  loading.value = false;
  if (!next.length) finished.value = true;
}
</script>

<template>
  <InfiniteScroll :loading="loading" :finished="finished" @arc-load-more="loadMore">
    <div v-for="item in items" :key="item.id">{{ item.name }}</div>
  </InfiniteScroll>
</template>
<script>
  import { InfiniteScroll } from '@arclux/arc-ui-svelte';

  let items = $state(initialItems);
  let loading = $state(false);
  let finished = $state(false);

  async function loadMore() {
    loading = true;
    const next = await fetchNextPage();
    items = [...items, ...next];
    loading = false;
    if (!next.length) finished = true;
  }
</script>

<InfiniteScroll {loading} {finished} on:arc-load-more={loadMore}>
  {#each items as item (item.id)}
    <div>{item.name}</div>
  {/each}
</InfiniteScroll>
import { Component } from '@angular/core';
import { InfiniteScroll } from '@arclux/arc-ui-angular';

@Component({
  imports: [InfiniteScroll],
  template: `
    <arc-infinite-scroll
      [loading]="loading"
      [finished]="finished"
      (arc-load-more)="loadMore()"
    >
      <div *ngFor="let item of items">{{ item.name }}</div>
    </arc-infinite-scroll>
  `,
})
export class FeedComponent {
  items = [];
  loading = false;
  finished = false;

  async loadMore() {
    this.loading = true;
    const next = await this.fetchNextPage();
    this.items.push(...next);
    this.loading = false;
    if (!next.length) this.finished = true;
  }
}
import { createSignal, For } from 'solid-js';
import { InfiniteScroll } from '@arclux/arc-ui-solid';

const [items, setItems] = createSignal(initialItems);
const [loading, setLoading] = createSignal(false);
const [finished, setFinished] = createSignal(false);

async function loadMore() {
  setLoading(true);
  const next = await fetchNextPage();
  setItems(prev => [...prev, ...next]);
  setLoading(false);
  if (!next.length) setFinished(true);
}

<InfiniteScroll loading={loading()} finished={finished()} onArcLoadMore={loadMore}>
  <For each={items()}>{item => <div>{item.name}</div>}</For>
</InfiniteScroll>
import { useState } from 'preact/hooks';
import { InfiniteScroll } from '@arclux/arc-ui-preact';

const [items, setItems] = useState(initialItems);
const [loading, setLoading] = useState(false);
const [finished, setFinished] = useState(false);

async function loadMore() {
  setLoading(true);
  const next = await fetchNextPage();
  setItems(prev => [...prev, ...next]);
  setLoading(false);
  if (!next.length) setFinished(true);
}

<InfiniteScroll loading={loading} finished={finished} onArcLoadMore={loadMore}>
  {items.map(item => <div key={item.id}>{item.name}</div>)}
</InfiniteScroll>

API

threshold number 200
Distance in pixels from the bottom of the content at which `arc-load-more` fires. Controls how eagerly new data is requested.
loading boolean false
When true, displays a spinner in the footer and suppresses additional `arc-load-more` events.
finished boolean false
When true, disconnects the observer and displays "No more items" text in the footer.
disabled boolean false
Disables the component, disconnects the observer, and reduces opacity to 40%.

Events

arc-load-more
Fired when the scroll sentinel enters the viewport, signaling more content should load

See Also