Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Augani/kael/llms.txt

Use this file to discover all available pages before exploring further.

Kael ships three virtualized list primitives that only render the items currently visible in the viewport. Choosing the right one depends on whether your items share a common height and how much control you need over element lifecycle. All three support scrolling, alignment, and overdraw tuning.

uniform_list()

uniform_list is the fastest option. It measures a single item once and assumes every item in the list has the same height. This lets it skip the full layout pass for off-screen items and jump straight to index-based positioning.

Signature

pub fn uniform_list<R>(
    id: impl Into<ElementId>,
    item_count: usize,
    f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
) -> UniformList
where
    R: IntoElement
The closure f receives the range of currently visible item indices and must return one element per index in that range.

Methods on UniformList

// Control scroll position through a handle stored on your view
pub fn track_scroll(self, handle: UniformListScrollHandle) -> Self

// Set sizing behavior (Auto or Infer)
pub fn with_sizing_behavior(self, behavior: ListSizingBehavior) -> Self

// Measure width from a specific item index instead of item 0
pub fn with_width_from_item(self, item_index: Option<usize>) -> Self

// Allow items to exceed the list width and enable horizontal scrolling
pub fn with_horizontal_sizing_behavior(self, behavior: ListHorizontalSizingBehavior) -> Self

// Flip item order so index 0 appears at the bottom (useful for chat logs)
pub fn y_flipped(self, y_flipped: bool) -> Self

// Add a visual decoration layer (indent guides, backgrounds, etc.)
pub fn with_decoration(self, decoration: impl UniformListDecoration + 'static) -> Self
UniformList also implements Styled and InteractiveElement, so you can apply the full set of layout and event methods.

UniformListScrollHandle

Store a UniformListScrollHandle on your view and pass it each frame via .track_scroll(). You can then call imperative scroll methods from event handlers:
// Scroll to an item — non-strict (no-ops if already visible)
pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy)

// Scroll to an item — strict (always applies even if already visible)
pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy)

// Scroll to an item leaving an offset margin of N items
pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize)

// Check whether the list has overflowing content
pub fn is_scrollable(&self) -> bool
ScrollStrategy controls where the target item lands: ScrollStrategy::Top, ScrollStrategy::Center, or ScrollStrategy::Bottom.

Example: file tree

// In your view struct:
scroll_handle: UniformListScrollHandle,

// In render:
uniform_list(cx.entity_id(), self.entries.len(), {
    let entries = self.entries.clone();
    move |range, _window, _cx| {
        range.map(|ix| {
            div()
                .flex()
                .items_center()
                .h(px(24.0))
                .child(entries[ix].name.clone())
                .into_any_element()
        })
        .collect()
    }
})
.track_scroll(self.scroll_handle.clone())
.w_full()
.flex_1()

recycling_list()

recycling_list handles heterogeneous lists where items can have different heights. It requires you to provide estimated heights for off-screen items up front, and can optionally recycle previously rendered elements back into new items of the same type to reduce allocation.

Signature

pub fn recycling_list<D>(id: impl Into<ElementId>, delegate: D) -> RecyclingList<D>
where
    D: ListDelegate
The id must remain stable across frames because the element uses it to persist measured heights and the scroll position.

The ListDelegate trait

You implement ListDelegate on a value stored in (or derived from) your view:
pub trait ListDelegate: 'static {
    // Total number of items
    fn item_count(&self) -> usize;

    // Height estimate for an item that has not been measured yet
    fn estimated_item_height(&self, ix: usize) -> Pixels;

    // Render the item at index ix
    fn render_item(&self, ix: usize, window: &mut Window, cx: &mut App) -> AnyElement;

    // Optional: return a TypeId key to enable element pooling for this index
    fn recycle_key(&self, ix: usize) -> Option<TypeId> { None }

    // Optional: render using a recycled element from the matching pool
    fn render_recycled_item(
        &self,
        ix: usize,
        recycled: Option<AnyElement>,
        window: &mut Window,
        cx: &mut App,
    ) -> AnyElement { /* default: calls render_item */ }
}

Methods on RecyclingList<D>

// Set sizing behavior
pub fn with_sizing_behavior(self, behavior: ListSizingBehavior) -> Self

// Set scroll alignment (Top or Bottom)
pub fn with_alignment(self, alignment: ListAlignment) -> Self

// Extra pixels rendered above and below the viewport (default: 200 px)
pub fn with_overdraw(self, overdraw: Pixels) -> Self

Example: message list with two item types

struct MessageListDelegate {
    items: Vec<MessageItem>,
}

impl ListDelegate for MessageListDelegate {
    fn item_count(&self) -> usize { self.items.len() }

    fn estimated_item_height(&self, ix: usize) -> Pixels {
        match &self.items[ix] {
            MessageItem::Text(_) => px(48.0),
            MessageItem::Image(_) => px(200.0),
        }
    }

    fn render_item(&self, ix: usize, _window: &mut Window, _cx: &mut App) -> AnyElement {
        match &self.items[ix] {
            MessageItem::Text(msg) => render_text_message(msg).into_any_element(),
            MessageItem::Image(img) => render_image_message(img).into_any_element(),
        }
    }
}

// In render:
recycling_list(cx.entity_id(), MessageListDelegate { items: self.items.clone() })
    .with_alignment(ListAlignment::Bottom)
    .w_full()
    .flex_1()
Use ListAlignment::Bottom for chat-style lists where new messages appear at the bottom. The list will scroll to keep the newest item in view.

list()

list is the lower-level primitive that both recycling_list and Kael’s internal tooling build on. Use it when you need fine-grained control over item measurement lifecycle, or when you need to splice items into the list state in response to data mutations.

Signature

pub fn list(
    state: ListState,
    render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,
) -> List
The ListState is stored on your view and passed into list() each render. The render closure is called only for visible items.

ListState

// Create a new ListState with item_count items, all with unknown heights
pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self

// Update the list after items are added, removed, or replaced
pub fn splice(&self, old_range: Range<usize>, count: usize)

// Variant that registers FocusHandles so focused off-screen items remain rendered
pub fn splice_focusable(
    &self,
    old_range: Range<usize>,
    focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
)

// Reset all items and scroll position
pub fn reset(&self, element_count: usize)

// Current number of items tracked by the state
pub fn item_count(&self) -> usize

Methods on List

pub fn with_sizing_behavior(self, behavior: ListSizingBehavior) -> Self
List also implements Styled.

Example: virtualized log viewer

// In your view struct:
list_state: ListState,

// In your view's new():
list_state: ListState::new(log_lines.len(), ListAlignment::Top, px(300.0)),

// When lines are appended:
self.list_state.splice(old_len..old_len, new_lines.len());

// In render:
list(
    self.list_state.clone(),
    {
        let lines = self.log_lines.clone();
        move |ix, _window, _cx| {
            div()
                .child(lines[ix].clone())
                .into_any_element()
        }
    },
)
.w_full()
.flex_1()
If an item’s height can change after it was first measured, call list_state.splice() over its range to invalidate the cached measurement. Failing to do so causes the list to misposition items below the changed entry.

Build docs developers (and LLMs) love