Virtual List
Use CVirtualList when you can server-render the complete collection and want the browser to skip off-screen layout and paint. Use CVirtualWindow when DOM size is the bottleneck and your application can supply each requested fixed-size server range. Both use CVirtualListItem for stable identity and arbitrary server-rendered content.
Keep complete server HTML
CVirtualList preserves every Item in the DOM and accessibility tree. It uses content-visibility: auto plus an intrinsic-size estimate, so it reduces rendering cost without reducing HTML transfer, DOM nodes, memory, Alpine roots, or Citry initialization.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class VirtualListAtAGlance(Component):
template = """
<c-CVirtualList aria_label="Build activity" c-estimated_item_size="64">
<c-for each="entry in entries">
<c-CVirtualListItem c-item_key="entry['key']">
<article>
<strong>{{ entry['title'] }}</strong><br />
<small>{{ entry['detail'] }}</small>
</article>
</c-CVirtualListItem>
</c-for>
</c-CVirtualList>
"""
css = """
:where([data-citry-ui-part="virtual-list"] article) {
padding: 0.75rem 1rem;
border-block-end: 1px solid color-mix(in srgb, currentColor 14%, transparent);
}
"""
def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]:
return {
"entries": [
{"key": f"build-{index}", "title": f"Build {2400 + index}", "detail": "Checks passed"}
for index in range(80)
]
}
preview = VirtualListAtAGlance()
preview # noqa: B018
Choose an estimated_item_size close to the average rendered block size. It is a browser layout hint, not a fixed height; rich Items may still wrap and grow. Stable item_key values preserve logical identity across server renders.
Supply a true DOM window
CVirtualWindow renders only the contiguous range supplied by the current server output. total_count, start_index, and item_size reserve the full scroll extent. The direct CVirtualListItem declarations are the committed range beginning at start_index.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class VirtualWindowExample(Component):
template = """
<section x-data="{last:'This preview shows the final server range'}">
<output x-text="last">This preview shows the final server range</output>
<c-CVirtualWindow
aria_label="Audit records"
c-total_count="36"
c-start_index="20"
c-item_size="48"
c-initial_index="20"
$c-props="{onRangeChange:(detail)=>last=`Requested ${detail.startIndex}-${detail.endIndex - 1}`}"
>
<c-for each="record in records">
<c-CVirtualListItem c-item_key="record['key']">
<span>{{ record['number'] }}</span> {{ record['label'] }}
</c-CVirtualListItem>
</c-for>
</c-CVirtualWindow>
</section>
"""
css = """
:where([data-citry-ui-part="item"]) {
display: flex;
align-items: center;
gap: 0.75rem;
padding-inline: 1rem;
border-block-end: 1px solid color-mix(in srgb, currentColor 12%, transparent);
}
:where([data-citry-ui-part="item"] > span) { color: GrayText; font-variant-numeric: tabular-nums; }
"""
def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]:
return {
"records": [
{"key": f"audit-{index}", "number": f"#{index + 1:05d}", "label": "Signed deployment record"}
for index in range(20, 36)
]
}
preview = VirtualWindowExample()
preview # noqa: B018
Pass onRangeChange through $c-props. The callback receives the desired overscanned half-open range, visible range, request ID, reason, and source event. It requests state; it never mutates or renders Item HTML. Fetch or render the new range, cancel superseded work in the application, and replace the component with the new start_index and Items.
The runtime marks the root aria-busy="true" and data-pending until the committed server range covers the current desired range. A missing callback leaves the current range usable. Callback failures are isolated and logged.
Keep window rows fixed
Every CVirtualWindow Item must occupy exactly item_size CSS pixels in the block axis. The component clips overflow to keep spacer geometry correct. Use bounded internal layout, truncation, or a larger row size; do not use a window for variable-height articles. The total scroll extent is limited to 16,000,000 CSS pixels because browser element-size limits are not portable.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class VirtualWindowControlled(Component):
template = """
<section x-data="{itemSize:56,overscan:2,last:'No request'}">
<label>Row size <input type="range" min="40" max="72" x-model.number="itemSize" /></label>
<label>Overscan <input type="range" min="0" max="8" x-model.number="overscan" /></label>
<output x-text="last">No request</output>
<c-CVirtualWindow
aria_label="Controlled geometry"
c-total_count="12"
c-item_size="56"
c-viewport_size="280"
$c-props="{
itemSize,
overscan,
onRangeChange:(detail)=>last=`${detail.reason}: ${detail.startIndex}-${detail.endIndex - 1}`,
}"
>
<c-for each="index in indexes">
<c-CVirtualListItem c-item_key="f'controlled-{index}'">Record {{ index + 1 }}</c-CVirtualListItem>
</c-for>
</c-CVirtualWindow>
</section>
"""
def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]:
return {"indexes": list(range(12))}
css = """
:where([data-citry-ui-part="item"]) { display:flex;align-items:center;padding-inline:1rem; }
:where(label) { display:inline-flex;gap:0.5rem;margin-inline-end:1rem; }
:where(output) { display:block;margin-block:0.5rem; }
"""
preview = VirtualWindowControlled()
preview # noqa: B018
overscan and itemSize are reactive client inputs. A valid Alpine change recomputes the requested range immediately. Invalid values log one diagnostic per episode and retain the previous valid value. Use a server render when the committed range or total count changes.
Accessibility and focus
Both owners render role="list" and CVirtualListItem renders role="listitem". A Window Item also receives exact aria-posinset and aria-setsize; spacers are hidden from assistive technology. focusable=True adds one viewport tab stop so keyboard users can scroll even when Items contain no controls.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class VirtualListAccessibility(Component):
template = """
<c-CStack>
<section>
<h2>Complete collection</h2>
<c-CVirtualList aria_label="All release notes" c-viewport_size="220">
<c-for each="index in complete_indexes">
<c-CVirtualListItem c-item_key="f'complete-{index}'">
<a c-href="f'#release-{index + 1}'">Release {{ index + 1 }}</a>
</c-CVirtualListItem>
</c-for>
</c-CVirtualList>
</section>
<section>
<h2>Supplied range</h2>
<c-CVirtualWindow
aria_label="Windowed release notes"
c-total_count="8"
c-item_size="44"
c-viewport_size="220"
>
<c-for each="index in window_indexes">
<c-CVirtualListItem c-item_key="f'window-{index}'">Release {{ index + 1 }}</c-CVirtualListItem>
</c-for>
</c-CVirtualWindow>
</section>
</c-CStack>
"""
css = """
:where([data-citry-ui-part="item"]) {
display:flex;align-items:center;padding-inline:0.75rem;min-block-size:44px;
}
"""
def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]:
return {"complete_indexes": list(range(16)), "window_indexes": list(range(8))}
preview = VirtualListAccessibility()
preview # noqa: B018
Use CVirtualList or ordinary pagination when assistive-technology users must browse the entire collection without application range requests. Windowing necessarily exposes only the supplied Items. Avoid windowing a long editable form. If a focused Item or the logical owner of an open overlay leaves the supplied range, ordinary Citry morph and owner-removal cleanup applies.
Server rendering and JavaScript
CVirtualList is CSS-only and remains fully useful without JavaScript. CVirtualWindow displays the supplied range at its correct offset without JavaScript but needs JavaScript to request another range. The runtime never clones, reparents, caches, or writes Item HTML and adds no generic client renderer.
Server morphs are authoritative. Stable Item keys preserve the Item/component relationship, and a retained root hands off its scroll offset across runtime replacement. The application still owns stale-request cancellation, loading, errors, retry, caching, and total-count changes.
Customize the viewport and Items
Use root class_, style, and attrs, Item equivalents, public variables, and documented part selectors. Window Item block size is owned geometry.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class VirtualListCustomization(Component):
template = """
<c-CVirtualList aria_label="Pinned environments" class_="environment-list" c-viewport_size="260">
<c-for each="environment in environments">
<c-CVirtualListItem c-item_key="environment['key']">
<strong>{{ environment['name'] }}</strong>
<span>{{ environment['region'] }}</span>
</c-CVirtualListItem>
</c-for>
</c-CVirtualList>
"""
css = """
:where(.environment-list) {
--cui-virtual-list-border: 2px solid #7c3aed;
--cui-virtual-list-radius: 1rem;
--cui-virtual-list-background: light-dark(#faf5ff, #2e1065);
--cui-virtual-list-item-background: light-dark(#fff, #1e1b4b);
}
:where(.environment-list [data-citry-ui-part="item"]) {
display:grid;
grid-template-columns:1fr auto;
gap:1rem;
padding:0.875rem 1rem;
margin:0.5rem;
border-radius:0.625rem;
}
:where(.environment-list [data-citry-ui-part="item"] span) { color:GrayText; }
"""
def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]:
return {
"environments": [
{"key": f"environment-{index}", "name": f"Service {index + 1}", "region": "eu-central"}
for index in range(24)
]
}
preview = VirtualListCustomization()
preview # noqa: B018
For print, CVirtualList expands and makes all Items visible. A CVirtualWindow can print only its supplied range; render a separate complete or paginated print view when the full collection matters.
Localization
The family owns no visible or accessibility text, announcements, parsing, formatting, filtering, sorting, or comparison. Localize aria_label and Item content in the application. The family therefore has no Citry UI catalog keys.
API reference
Inputs
CVirtualList server inputs
Server inputs are passed in a template through <c-CVirtualList ... /> or in Python through CVirtualList(...).
| Input | Type | Default | Effect |
|---|---|---|---|
aria_label | str | None | None | Optionally names the complete-DOM list. |
estimated_item_size | int | 48 | Sets the positive pixel intrinsic-size estimate used while off-screen Item rendering is skipped. |
viewport_size | int | 400 | Sets the positive initial viewport block size in CSS pixels. |
focusable | bool | True | Adds or removes the root tabindex=0 keyboard-scroll stop. |
class_ | CClassValue | None (CClassValue) | None | Adds classes to the list viewport. |
style | CStyleValue | None (CStyleValue) | None | Adds styles before owned viewport geometry variables. |
attrs | Mapping[str, object] | None | None | Adds copied allowed viewport attributes without replacing owned roles geometry state or runtime markers. |
CVirtualWindow server inputs
Server inputs are passed in a template through <c-CVirtualWindow ... /> or in Python through CVirtualWindow(...).
| Input | Type | Default | Effect |
|---|---|---|---|
total_count | int | required | Sets the exact nonnegative logical collection size. |
start_index | int | 0 | Sets the nonnegative logical index of the first supplied Item. |
item_size | int | 48 | Sets the positive fixed Item stride in CSS pixels; total extent cannot exceed 16000000 pixels. |
viewport_size | int | 400 | Sets the positive initial viewport block size in CSS pixels. |
overscan | int | 3 | Requests zero through one hundred Items before and after the visible range. |
initial_index | int | 0 | Sets the one-shot nonnegative initial scroll index and clamps it to the collection. |
aria_label | str | None | None | Optionally names the windowed list. |
focusable | bool | True | Adds or removes the root tabindex=0 keyboard-scroll stop. |
class_ | CClassValue | None (CClassValue) | None | Adds classes to the Window viewport. |
style | CStyleValue | None (CStyleValue) | None | Adds styles before owned viewport geometry variables. |
attrs | Mapping[str, object] | None | None | Adds copied allowed viewport attributes without replacing owned roles geometry state or runtime markers. |
CVirtualWindow client inputs
Client inputs are passed in the browser through the $c-props="{ ... }" attribute on <c-CVirtualWindow />.
| Input | Type | Omitted behavior | Effect |
|---|---|---|---|
overscan | int | Uses the server value; null is invalid and retains the last valid value. | Reactively changes the requested buffer from zero through one hundred Items. |
itemSize | number | Uses the server value; null is invalid and retains the last valid value. | Reactively changes fixed pixel geometry while the resulting total extent stays within the family limit. |
onRangeChange | function | Omission or null selects no component callback. | Receives newest distinct range requests without committing server state. |
CVirtualListItem server inputs
Server inputs are passed in a template through <c-CVirtualListItem ... /> or in Python through CVirtualListItem(...).
| Input | Type | Default | Effect |
|---|---|---|---|
item_key | str | required | Supplies nonempty stable identity unique within the owning logical collection or supplied range. |
class_ | CClassValue | None (CClassValue) | None | Adds classes to the rendered list Item. |
style | CStyleValue | None (CStyleValue) | None | Adds styles to the Item; Window fixed block size remains owned. |
attrs | Mapping[str, object] | None | None | Adds copied allowed Item attributes without replacing owned roles positions identity or runtime markers. |
Slots
Slots are passed as nested content or <c-fill> tags in a template, or through the slots={...} argument in Python.
CVirtualList slots
| Slot | Required | Data | Fallback |
|---|---|---|---|
default | no | {} (CVirtualListDefaultSlotData) | Empty list; accepts only CVirtualListItem declarations. |
CVirtualWindow slots
| Slot | Required | Data | Fallback |
|---|---|---|---|
default | no | {} (CVirtualListDefaultSlotData) | Empty supplied range; accepts only CVirtualListItem declarations. |
CVirtualListItem slots
| Slot | Required | Data | Fallback |
|---|---|---|---|
default | yes | {index, item_key, set_size, strategy} (CVirtualListItemDefaultSlotData) | None. |
Events
Component events are callback inputs supplied through $c-props. Native browser events remain available through Alpine @... attributes.
CVirtualWindow events
| Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior |
|---|---|---|---|---|
onRangeChange | (detail: CVirtualListRangeChangeDetail) => void (CVirtualListRangeChangeDetail) | The visible overscanned range is not covered by the committed server range. | {startIndex, endIndex, visibleStartIndex, visibleEndIndex, requestId, reason, sourceEvent} (CVirtualListRangeChangeDetail) | Animation-frame-coalesced request only. The application supplies a new server range and owns cancellation supersession loading error and retry. |
Methods
-
CSS
CSS variables to theme the components. Set them on an ancestor or the component itself.
CVirtualList CSS variables
Apply these variables to CVirtualList or one of its ancestors.
| Variable | Type | Purpose | Default |
|---|---|---|---|
--cui-virtual-list-viewport-size | length | Viewport block size read by both root owners. | Server viewport_size; 400px |
--cui-virtual-list-item-size | length | Complete-DOM intrinsic estimate or owned Window Item stride. | Server estimate or item_size; 48px |
--cui-virtual-list-border | complete border value | Viewport border. | Adaptive 1px solid neutral |
--cui-virtual-list-radius | length | Viewport corner radius. | 0.625rem |
--cui-virtual-list-background | color | Viewport background. | Canvas |
--cui-virtual-list-item-background | color | Item background. | transparent |
Attributes
HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only.
CVirtualList attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
role | Root viewport div | list | Exposes noninteractive list semantics. |
aria-label | Root viewport div | string | absent | Optional application-localized list name. |
tabindex | Root viewport div | 0 | absent | Adds keyboard-scroll focus when focusable is true. |
data-strategy | Root viewport div | content-visibility | Identifies complete-DOM containment behavior. |
CVirtualWindow attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
role | Root viewport div | list | Exposes noninteractive list semantics. |
aria-label | Root viewport div | string | absent | Optional application-localized list name. |
aria-busy | Root viewport div | true | absent | Present while the current desired range is not covered. |
tabindex | Root viewport div | 0 | absent | Adds keyboard-scroll focus when focusable is true. |
data-strategy | Root viewport div | window | Identifies true controlled window behavior. |
data-pending | Root viewport div | present | absent | Mirrors an uncovered desired range. |
data-start-index | Root viewport div | nonnegative-integer-string | Mirrors the committed server range start. |
data-total-count | Root viewport div | nonnegative-integer-string | Mirrors logical collection size. |
CVirtualListItem attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
role | Item div | listitem | Exposes one noninteractive list item. |
data-index | Item div | nonnegative-integer-string | Exposes settled logical zero-based position. |
data-item-key | Item div | string | Exposes stable server identity. |
aria-posinset | Window Item div | positive-integer-string | absent | Exposes one-based logical position only in CVirtualWindow. |
aria-setsize | Window Item div | nonnegative-integer-string | absent | Exposes total logical size only in CVirtualWindow. |
Selectors
Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing.
CVirtualList selectors
| Selector | Element | Purpose |
|---|---|---|
[data-citry-ui-part="virtual-list"] | Root viewport div | Root attrs and viewport customization destination for both owners. |
[data-citry-ui-part="track"] | Direct track div | Contains spacers and supplied Items. |
[data-citry-ui-part="item"] | Item div | Stable Item attrs content and customization destination. |
[data-citry-ui-part="spacer"] | Window-only aria-hidden div | Reserves omitted range space; geometry is owned. |
Interfaces
Aliases and data shapes referenced above.
Input type aliases
| Interface | Definition |
|---|---|
CVirtualListStrategy | Literal["content-visibility", "window"] |
CVirtualListRangeReason | Literal["initial", "scroll", "resize", "configuration"] |
CClassValue | str | Mapping[str, bool] | Sequence[CClassValue] |
CStyleValue | str | Mapping[str, object] | Sequence[CStyleValue] |
CVirtualListDefaultSlotData
Empty dataclass: {}.
CVirtualListItemDefaultSlotData
| Field | Type | Default | Meaning |
|---|---|---|---|
index | int | - | Settled logical zero-based Item position. |
item_key | str | - | Stable authored Item identity. |
set_size | int | - | Complete declaration count or Window total_count. |
strategy | CVirtualListStrategy (CVirtualListStrategy) | - | Identifies the owning complete-DOM or Window behavior. |
CVirtualListRangeChangeDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
startIndex | int | - | Inclusive requested overscanned range start. |
endIndex | int | - | Exclusive requested overscanned range end. |
visibleStartIndex | int | - | Inclusive geometrically visible range start. |
visibleEndIndex | int | - | Exclusive geometrically visible range end. |
requestId | int | - | Monotonically increasing instance-local request identifier. |
reason | CVirtualListRangeReason (CVirtualListRangeReason) | - | Geometry trigger that scheduled the latest request frame. |
sourceEvent | Event | null | - | Latest native scroll event when reason is scroll; otherwise null. |
Translation keys
-