Theme
Version
GitHub PyPI Discord
On this page

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.

Keep a complete virtualized list
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.

Supply a fixed server window
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.

Tune range geometry
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.

Compare complete and windowed semantics
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.

Customize Virtual List
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(...).

InputTypeDefaultEffect
aria_labelstr | NoneNoneOptionally names the complete-DOM list.
estimated_item_sizeint48Sets the positive pixel intrinsic-size estimate used while off-screen Item rendering is skipped.
viewport_sizeint400Sets the positive initial viewport block size in CSS pixels.
focusableboolTrueAdds or removes the root tabindex=0 keyboard-scroll stop.
class_CClassValue | None (CClassValue)NoneAdds classes to the list viewport.
styleCStyleValue | None (CStyleValue)NoneAdds styles before owned viewport geometry variables.
attrsMapping[str, object] | NoneNoneAdds 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(...).

InputTypeDefaultEffect
total_countintrequiredSets the exact nonnegative logical collection size.
start_indexint0Sets the nonnegative logical index of the first supplied Item.
item_sizeint48Sets the positive fixed Item stride in CSS pixels; total extent cannot exceed 16000000 pixels.
viewport_sizeint400Sets the positive initial viewport block size in CSS pixels.
overscanint3Requests zero through one hundred Items before and after the visible range.
initial_indexint0Sets the one-shot nonnegative initial scroll index and clamps it to the collection.
aria_labelstr | NoneNoneOptionally names the windowed list.
focusableboolTrueAdds or removes the root tabindex=0 keyboard-scroll stop.
class_CClassValue | None (CClassValue)NoneAdds classes to the Window viewport.
styleCStyleValue | None (CStyleValue)NoneAdds styles before owned viewport geometry variables.
attrsMapping[str, object] | NoneNoneAdds 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 />.

InputTypeOmitted behaviorEffect
overscanintUses the server value; null is invalid and retains the last valid value.Reactively changes the requested buffer from zero through one hundred Items.
itemSizenumberUses 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.
onRangeChangefunctionOmission 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(...).

InputTypeDefaultEffect
item_keystrrequiredSupplies nonempty stable identity unique within the owning logical collection or supplied range.
class_CClassValue | None (CClassValue)NoneAdds classes to the rendered list Item.
styleCStyleValue | None (CStyleValue)NoneAdds styles to the Item; Window fixed block size remains owned.
attrsMapping[str, object] | NoneNoneAdds 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

SlotRequiredDataFallback
defaultno{} (CVirtualListDefaultSlotData)Empty list; accepts only CVirtualListItem declarations.

CVirtualWindow slots

SlotRequiredDataFallback
defaultno{} (CVirtualListDefaultSlotData)Empty supplied range; accepts only CVirtualListItem declarations.

CVirtualListItem slots

SlotRequiredDataFallback
defaultyes{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

EventSignatureTrigger and timingDetailControlled 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.

VariableTypePurposeDefault
--cui-virtual-list-viewport-sizelengthViewport block size read by both root owners.Server viewport_size; 400px
--cui-virtual-list-item-sizelengthComplete-DOM intrinsic estimate or owned Window Item stride.Server estimate or item_size; 48px
--cui-virtual-list-bordercomplete border valueViewport border.Adaptive 1px solid neutral
--cui-virtual-list-radiuslengthViewport corner radius.0.625rem
--cui-virtual-list-backgroundcolorViewport background.Canvas
--cui-virtual-list-item-backgroundcolorItem background.transparent

Attributes

HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only.

CVirtualList attributes

AttributeElementTypeMeaning
roleRoot viewport divlistExposes noninteractive list semantics.
aria-labelRoot viewport divstring | absentOptional application-localized list name.
tabindexRoot viewport div0 | absentAdds keyboard-scroll focus when focusable is true.
data-strategyRoot viewport divcontent-visibilityIdentifies complete-DOM containment behavior.

CVirtualWindow attributes

AttributeElementTypeMeaning
roleRoot viewport divlistExposes noninteractive list semantics.
aria-labelRoot viewport divstring | absentOptional application-localized list name.
aria-busyRoot viewport divtrue | absentPresent while the current desired range is not covered.
tabindexRoot viewport div0 | absentAdds keyboard-scroll focus when focusable is true.
data-strategyRoot viewport divwindowIdentifies true controlled window behavior.
data-pendingRoot viewport divpresent | absentMirrors an uncovered desired range.
data-start-indexRoot viewport divnonnegative-integer-stringMirrors the committed server range start.
data-total-countRoot viewport divnonnegative-integer-stringMirrors logical collection size.

CVirtualListItem attributes

AttributeElementTypeMeaning
roleItem divlistitemExposes one noninteractive list item.
data-indexItem divnonnegative-integer-stringExposes settled logical zero-based position.
data-item-keyItem divstringExposes stable server identity.
aria-posinsetWindow Item divpositive-integer-string | absentExposes one-based logical position only in CVirtualWindow.
aria-setsizeWindow Item divnonnegative-integer-string | absentExposes 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

SelectorElementPurpose
[data-citry-ui-part="virtual-list"]Root viewport divRoot attrs and viewport customization destination for both owners.
[data-citry-ui-part="track"]Direct track divContains spacers and supplied Items.
[data-citry-ui-part="item"]Item divStable Item attrs content and customization destination.
[data-citry-ui-part="spacer"]Window-only aria-hidden divReserves omitted range space; geometry is owned.

Interfaces

Aliases and data shapes referenced above.

Input type aliases

InterfaceDefinition
CVirtualListStrategyLiteral["content-visibility", "window"]
CVirtualListRangeReasonLiteral["initial", "scroll", "resize", "configuration"]
CClassValuestr | Mapping[str, bool] | Sequence[CClassValue]
CStyleValuestr | Mapping[str, object] | Sequence[CStyleValue]

CVirtualListDefaultSlotData

Empty dataclass: {}.

CVirtualListItemDefaultSlotData

FieldTypeDefaultMeaning
indexint-Settled logical zero-based Item position.
item_keystr-Stable authored Item identity.
set_sizeint-Complete declaration count or Window total_count.
strategyCVirtualListStrategy (CVirtualListStrategy)-Identifies the owning complete-DOM or Window behavior.

CVirtualListRangeChangeDetail

FieldTypeDefaultMeaning
startIndexint-Inclusive requested overscanned range start.
endIndexint-Exclusive requested overscanned range end.
visibleStartIndexint-Inclusive geometrically visible range start.
visibleEndIndexint-Exclusive geometrically visible range end.
requestIdint-Monotonically increasing instance-local request identifier.
reasonCVirtualListRangeReason (CVirtualListRangeReason)-Geometry trigger that scheduled the latest request frame.
sourceEventEvent | null-Latest native scroll event when reason is scroll; otherwise null.

Translation keys

-