Infinite Scroll
CInfiniteScroll owns the request boundary around results. Your application still owns the records, request, response, item identity, and rerender.
Keep a server fallback
Set action_name inside a form to render a named Load more submit button. It uses formnovalidate, so unrelated incomplete fields do not block pagination.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class InfiniteScrollAtAGlance(Component):
template = """
<section x-data>
<form @submit.prevent>
<c-CInfiniteScroll
aria_label="Activity feed"
action_name="feed_action"
c-auto="False"
$c-props="{loading, hasMore, onLoadMore: loadNextPage}"
>
<ol>
<li>Created the project</li>
<li>Invited the design team</li>
<li>Published the brief</li>
<template x-for="activity in moreActivities" :key="activity.id">
<li x-text="activity.label"></li>
</template>
</ol>
</c-CInfiniteScroll>
</form>
<output aria-live="polite" x-text="`Loaded ${3 + moreActivities.length} activities`">
Loaded 3 activities
</output>
</section>
"""
js = """
$component(({ scope }) => {
const pages = [
[
{ id: 4, label: 'Received legal approval' },
{ id: 5, label: 'Scheduled the launch' },
],
[
{ id: 6, label: 'Opened early access' },
{ id: 7, label: 'Collected the first responses' },
],
];
scope.moreActivities = [];
scope.loading = false;
scope.hasMore = true;
let page = 0;
scope.loadNextPage = detail => {
// This static preview handles the named action locally. A server page
// lets the submit continue and returns the next keyed result page.
detail.sourceEvent?.preventDefault();
if (scope.loading || !scope.hasMore) return;
scope.loading = true;
return new Promise(resolve => setTimeout(() => {
scope.moreActivities.push(...pages[page]);
page += 1;
scope.hasMore = page < pages.length;
scope.loading = false;
resolve();
}, 220));
};
});
"""
preview = InfiniteScrollAtAGlance()
preview # noqa: B018
The preview intercepts the named action and appends two bounded dummy pages so you can exercise the complete state change here. In an application, let the named submit continue and return the next keyed result page from the server.
Keep loading automatically
Pass onLoadMore through $c-props. When the sentinel reaches root_margin, the callback receives {reason: 'intersection', sourceEvent: null}. Button activation uses button or retry and includes the native event.
Show code
# ruff: noqa: E501 - embedded Citry template attributes remain readable as authored HTML
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class InfiniteScrollAutomatic(Component):
template = """
<section x-data>
<p>Scroll to the end of the clipped result feed.</p>
<c-CInfiniteScroll
aria_label="Search results"
c-style="{'max-block-size': '12rem', 'overflow': 'auto', 'overflow-anchor': 'none', 'padding-inline-end': '0.25rem'}"
$c-props="{loading, hasMore, onLoadMore: loadNextPage}"
>
<ol>
<li>Search result 1</li><li>Search result 2</li><li>Search result 3</li><li>Search result 4</li>
<li>Search result 5</li><li>Search result 6</li><li>Search result 7</li><li>Search result 8</li>
<template x-for="result in moreResults" :key="result.id">
<li x-text="result.label"></li>
</template>
</ol>
</c-CInfiniteScroll>
<output aria-live="polite" x-text="`Loaded ${8 + moreResults.length} results`">
Loaded 8 results
</output>
</section>
"""
js = """
$component(({ scope }) => {
scope.moreResults = [];
scope.loading = false;
scope.hasMore = true;
let nextResult = 9;
scope.loadNextPage = () => {
if (scope.loading) return;
scope.loading = true;
return new Promise(resolve => setTimeout(() => {
const page = Array.from({ length: 4 }, () => {
const id = nextResult++;
return { id, label: `Search result ${id}` };
});
scope.moreResults.push(...page);
scope.loading = false;
resolve();
}, 260));
};
});
"""
preview = InfiniteScrollAutomatic()
preview # noqa: B018
Update loading, error, or hasMore from application state. Only one request may be in progress at a time. A state change, nested content append, or returned Promise settling releases that request lock and permits a later request.
The automatic preview uses a clipped result feed. Scroll that feed to its end to append another four generated results. It deliberately has no final page, so every fresh trip to the end loads more. Production code sets hasMore=False when its data source returns no continuation.
Compose with Virtual List
Infinite Scroll answers when to load. Virtual List answers which known items to render. Nest either component in the default slot without sharing their state machines.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class InfiniteScrollVirtualList(Component):
template = """
<section x-data>
<c-CInfiniteScroll
aria_label="Audit log"
c-auto="False"
$c-props="{loading, hasMore, onLoadMore: loadSnapshot}"
>
<div x-bind:hidden="expanded">
<c-CVirtualList aria_label="Loaded audit records" c-viewport_size="180">
<c-CVirtualListItem item_key="event-1">Signed in</c-CVirtualListItem>
<c-CVirtualListItem item_key="event-2">Changed billing contact</c-CVirtualListItem>
<c-CVirtualListItem item_key="event-3">Exported report</c-CVirtualListItem>
</c-CVirtualList>
</div>
<div hidden x-bind:hidden="!expanded">
<c-CVirtualList aria_label="Loaded audit records" c-viewport_size="180">
<c-CVirtualListItem item_key="event-1">Signed in</c-CVirtualListItem>
<c-CVirtualListItem item_key="event-2">Changed billing contact</c-CVirtualListItem>
<c-CVirtualListItem item_key="event-3">Exported report</c-CVirtualListItem>
<c-CVirtualListItem item_key="event-4">Created an API token</c-CVirtualListItem>
<c-CVirtualListItem item_key="event-5">Updated tax details</c-CVirtualListItem>
<c-CVirtualListItem item_key="event-6">Invited a reviewer</c-CVirtualListItem>
</c-CVirtualList>
</div>
</c-CInfiniteScroll>
<output aria-live="polite" x-text="expanded ? 'Showing 6 audit records' : 'Showing 3 audit records'">
Showing 3 audit records
</output>
</section>
"""
js = """
$component(({ scope }) => {
scope.expanded = false;
scope.loading = false;
scope.hasMore = true;
scope.loadSnapshot = () => {
if (scope.loading || !scope.hasMore) return;
scope.loading = true;
return new Promise(resolve => setTimeout(() => {
scope.expanded = true;
scope.hasMore = false;
scope.loading = false;
resolve();
}, 220));
};
});
"""
preview = InfiniteScrollVirtualList()
preview # noqa: B018
This static preview swaps from a three-record server snapshot to a six-record snapshot. A real owner returns the newly rendered keyed collection.
Show errors and retry
Set error=True after a failed request. The same stable action becomes Try again, and its callback reason becomes retry. Automatic observation pauses until that explicit retry or another state change clears the error.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class InfiniteScrollErrorRetry(Component):
template = """
<section x-data>
<c-CInfiniteScroll
aria_label="Orders"
c-auto="False"
$c-props="{loading, error, hasMore, onLoadMore: retryPage}"
>
<ul>
<li>Order #1042</li>
<li>Order #1041</li>
<template x-for="order in recoveredOrders" :key="order.id">
<li x-text="order.label"></li>
</template>
</ul>
</c-CInfiniteScroll>
<output aria-live="polite" x-text="recovered ? 'Orders recovered' : 'Last request failed'">
Last request failed
</output>
</section>
"""
js = """
$component(({ scope }) => {
scope.recoveredOrders = [];
scope.loading = false;
scope.error = true;
scope.hasMore = true;
scope.recovered = false;
scope.retryPage = () => {
if (scope.loading || !scope.hasMore) return;
scope.error = false;
scope.loading = true;
return new Promise(resolve => setTimeout(() => {
scope.recoveredOrders.push(
{ id: 1040, label: 'Order #1040' },
{ id: 1039, label: 'Order #1039' },
);
scope.recovered = true;
scope.hasMore = false;
scope.loading = false;
resolve();
}, 260));
};
});
"""
preview = InfiniteScrollErrorRetry()
preview # noqa: B018
Use named form actions
The observer never submits a form. A named action remains explicit and useful without a browser runtime.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class InfiniteScrollServerAction(Component):
template = """
<form x-data @submit.prevent="loadServerPage($event)">
<label>Search query <input name="query" required /></label>
<c-CInfiniteScroll
aria_label="Server search results"
action_name="result_action"
action_value="next:2"
c-auto="False"
$c-props="{loading, hasMore}"
>
<ol>
<li>Camera body comparison</li>
<li>Lens mount guide</li>
<template x-for="result in moreResults" :key="result.id">
<li x-text="result.label"></li>
</template>
</ol>
</c-CInfiniteScroll>
<output aria-live="polite" x-text="acceptedAction">Waiting for a named action</output>
</form>
"""
js = """
$component(({ scope }) => {
scope.moreResults = [];
scope.loading = false;
scope.hasMore = true;
scope.acceptedAction = 'Waiting for a named action';
scope.loadServerPage = event => {
const submitter = event.submitter;
if (!submitter || scope.loading || !scope.hasMore) return;
scope.acceptedAction = `${submitter.name}=${submitter.value}`;
scope.loading = true;
setTimeout(() => {
scope.moreResults.push(
{ id: 3, label: 'Mirrorless travel kit' },
{ id: 4, label: 'Low-light autofocus test' },
);
scope.hasMore = false;
scope.loading = false;
}, 240);
};
});
"""
preview = InfiniteScrollServerAction()
preview # noqa: B018
The preview reports the accepted submitter name and value, then appends its dummy response without leaving the frame. A production form sends that named action to its application endpoint.
Announce bounded state
Give a mixed page an aria_label. Loading, error, and end messages use a polite status. The sentinel is hidden from assistive technology and the button stays keyboard reachable.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class InfiniteScrollAccessibility(Component):
template = """
<c-CInfiniteScroll aria_label="Completed notifications" c-has_more="False">
<ul><li>Backup completed</li><li>Invoice sent</li></ul>
</c-CInfiniteScroll>
"""
preview = InfiniteScrollAccessibility()
preview # noqa: B018
The five default strings are Citry UI messages. Explicit label inputs opt that one output out of catalog-driven browser updates.
API reference
Inputs
CInfiniteScroll server inputs
Server inputs are passed in a template through <c-CInfiniteScroll ... /> or in Python through CInfiniteScroll(...).
| Input | Type | Default | Effect |
|---|---|---|---|
id | str | None | generated | Sets the root ID. |
aria_label | str | None | None | Names the root and gives it region semantics. |
has_more | bool | True | Controls whether another page exists. |
loading | bool | False | Shows pending state and suppresses requests. |
error | bool | False | Shows error state while changing the action to Retry and pausing automatic observation. |
disabled | bool | False | Disables requests and the action. |
auto | bool | True | Enables Intersection Observer requests when a callback exists and no loading error disabled or end state blocks them. |
root_margin | str | "0px 0px 240px 0px" | Sets the observer prefetch margin. |
threshold | float | 0 | Sets a finite observer threshold from zero through one. |
action_name | str | None | None | Makes the action a named submit button when supplied. |
action_value | str | "load-more" | Sets the submit button value. |
load_more_label | str | "Load more" | Overrides Load more text. |
retry_label | str | "Try again" | Overrides Retry text. |
loading_label | str | "Loading more results" | Overrides pending status text. |
error_label | str | "More results could not be loaded" | Overrides error status text. |
end_label | str | "No more results" | Overrides end status text. |
class_ | CClassValue | None (CClassValue) | None | Adds root classes. |
style | CStyleValue | None (CStyleValue) | None | Adds root styles. |
attrs | Mapping[str, object] | None | None | Adds copied allowed root attributes. |
CInfiniteScroll client inputs
Client inputs are passed in the browser through the $c-props="{ ... }" attribute on <c-CInfiniteScroll />.
| Input | Type | Omitted behavior | Effect |
|---|---|---|---|
hasMore | boolean | Uses the server value. | Reactively controls whether another page exists. |
loading | boolean | Uses the server value. | Reactively controls pending state. |
error | boolean | Uses the server value. | Reactively controls retry state. |
disabled | boolean | Uses the server value. | Reactively disables requests. |
auto | boolean | Uses the server value. | Reactively enables observation. |
onLoadMore | function | The observer stays inactive and the native button remains available. | Receives each load request and may return a Promise. |
Slots
Slots are passed as nested content or <c-fill> tags in a template, or through the slots={...} argument in Python.
CInfiniteScroll slots
| Slot | Required | Data | Fallback |
|---|---|---|---|
default | no | {} (CInfiniteScrollDefaultSlotData) | Empty result content. |
Events
Component events are callback inputs supplied through $c-props. Native browser events remain available through Alpine @... attributes.
CInfiniteScroll events
| Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior |
|---|---|---|---|---|
onLoadMore | (detail: CInfiniteScrollLoadDetail) => void | Promise<void> (CInfiniteScrollLoadDetail) | An enabled action is activated or its observed sentinel intersects. | {reason, sourceEvent} (CInfiniteScrollLoadDetail) | Requests data without mutating result content or submitting from the observer. |
Methods
-
CSS
CSS variables to theme the components. Set them on an ancestor or the component itself.
CInfiniteScroll CSS variables
Apply these variables to CInfiniteScroll or one of its ancestors.
| Variable | Type | Purpose | Default |
|---|---|---|---|
--cui-infinite-scroll-gap | length | Gap among content status action and sentinel. | 0.875rem |
--cui-infinite-scroll-action-border | complete border | Action boundary. | Adaptive 1px neutral |
--cui-infinite-scroll-action-surface | color | Action surface. | Canvas |
--cui-infinite-scroll-action-radius | length | Action corners. | 0.625rem |
--cui-infinite-scroll-focus | color | Action focus ring. | Highlight |
--cui-infinite-scroll-muted | color | Status text. | Adaptive muted CanvasText |
Attributes
HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only.
CInfiniteScroll attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
aria-busy | Content | true | false | Reflects loading without delaying sibling status announcements. |
data-loading | Root | present | absent | Reflects loading. |
data-error | Root | present | absent | Reflects visible retry state. |
data-end | Root | present | absent | Reflects exhausted results. |
data-disabled | Root | present | absent | Reflects disabled requests. |
data-auto | Root | present | absent | Reflects observation preference. |
Selectors
Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing.
CInfiniteScroll selectors
| Selector | Element | Purpose |
|---|---|---|
[data-citry-ui-part="infinite-scroll"] | Root | Request boundary and state destination. |
[data-citry-ui-part="content"] | Content div | Server-owned results. |
[data-citry-ui-part="status"] | Polite status | Pending error and end announcements. |
[data-citry-ui-part="action"] | Native button | Explicit Load more or Retry path. |
[data-citry-ui-part="sentinel"] | Hidden span | Intersection observation target. |
Interfaces
Aliases and data shapes referenced above.
Input type aliases
| Interface | Definition |
|---|---|
CInfiniteScrollReason | Literal["button", "intersection", "retry"] |
CClassValue | str | Mapping[str, bool] | Sequence[CClassValue] |
CStyleValue | str | Mapping[str, object] | Sequence[CStyleValue] |
CInfiniteScrollDefaultSlotData
Empty dataclass: {}.
CInfiniteScrollLoadDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
reason | CInfiniteScrollReason (CInfiniteScrollReason) | - | Button intersection or retry request source. |
sourceEvent | object | None | - | Native click Event or null for intersection. |
Translation keys
Catalog keys used by this family. An explicit component input or slot listed in Override takes precedence over the catalog for that instance.
CInfiniteScroll translation keys
| Key | Purpose | Variables | Override | Browser updates |
|---|---|---|---|---|
citry-ui-infinite-scroll-load-more | Labels the ordinary load action. | None. | load_more_label | Stable $c-tr text. |
citry-ui-infinite-scroll-retry | Labels the retry action. | None. | retry_label | Stable $c-tr text. |
citry-ui-infinite-scroll-loading | Announces a pending request. | None. | loading_label | Stable $c-tr text. |
citry-ui-infinite-scroll-error | Announces a failed request. | None. | error_label | Stable $c-tr text. |
citry-ui-infinite-scroll-end | Announces exhausted results. | None. | end_label | Stable $c-tr text. |