Lists
LazyColumn and LazyRow draw only the rows on screen. @for inside one becomes items(), not a loop that draws everything up front.
LazyColumn
A vertical list that renders only its visible rows:
<LazyColumn class="gap-12">
@for (item in items) {
<Card>{item.name}</Card>
}
</LazyColumn> Inside a LazyColumn or LazyRow, @for compiles to items(), and each direct child becomes an item { }.
LazyRow
A horizontal list, the same shape:
<LazyRow class="gap-8">
@for (category in categories) {
<Card class="w-120">
<Image src={category.icon} />
<Text>{category.name}</Text>
</Card>
}
</LazyRow> key
key is part of @for, not a prop on the child element. It gives
each row an identity so its state moves with it when the list reorders; without one,
state stays with the position:
@for (item in items, key = { it.id }) {
<Card>{item.name}</Card>
} Empty state
empty { } runs when the collection has nothing in it:
<LazyColumn>
@for (item in items, key = { it.id }) {
<Card>{item.name}</Card>
} empty {
<Column class="w-full h-full items-center justify-center gap-8">
<Icon name="lucide:inbox" size={48} color="muted-foreground" />
<Text class="text-muted-foreground">No items yet</Text>
</Column>
}
</LazyColumn> See it happen
Switch between these to see identity and the empty state added one step at a time, compiled live.
data class Item(val id: String, val name: String)
var items: List<Item> = $state(listOf())
<LazyColumn class="gap-12">
@for (item in items) {
<Card><Text>{item.name}</Text></Card>
}
</LazyColumn>@for repeats one Card per item, inside a LazyColumn so only the visible rows are drawn.
Item animation
Use animate:flip for reordering, on a keyed row:
<LazyColumn class="gap-8">
@for (item in items, key = { it.id }) {
<Card animate:flip={{ duration: 200 }}>
<Text>{item.name}</Text>
</Card>
}
</LazyColumn> Examples
News feed
data class Article(val id: String, val title: String, val summary: String, val image: String)
var articles: List<Article> = $state(listOf())
$onMount { articles = $fetch("https://api.example.com/articles") }
<LazyColumn class="gap-16 p-16">
@for (article in articles, key = { it.id }) {
<Card onClick={() => $navigate("/article/${article.id}")}>
<Image src={article.image} class="h-200" />
<Column class="p-16 gap-8">
<Text class="text-lg font-bold">{article.title}</Text>
<Text class="text-sm text-muted-foreground">{article.summary}</Text>
</Column>
</Card>
}
</LazyColumn> Horizontal category list
<Column class="gap-16">
<Text class="text-xl font-bold px-16">Categories</Text>
<LazyRow class="gap-12 px-16">
@for (category in categories, key = { it.id }) {
<Card class="w-100" onClick={() => selectCategory(category.id)}>
<Image src={category.icon} class="w-100 h-100" />
<Text class="text-xs text-center p-8">{category.name}</Text>
</Card>
}
</LazyRow>
</Column> Contacts with headers
A sticky section header is markup ahead of the emitted items — there is no sticky prop:
data class Group(val letter: String, val contacts: List<Contact>)
var contacts: List<Group> = $state(groupContactsByLetter())
<LazyColumn>
@for (group in contacts, key = { it.letter }) {
<Text class="font-bold bg-muted px-16 py-8">{group.letter}</Text>
@for (contact in group.contacts, key = { it.id }) {
<Row class="gap-12 p-16 items-center" onClick={() => $navigate("/contact/${contact.id}")}>
<Image src={contact.avatar} class="w-48 h-48 rounded-full" />
<Column>
<Text>{contact.name}</Text>
<Text class="text-sm text-muted-foreground">{contact.phone}</Text>
</Column>
</Row>
}
}
</LazyColumn> Search results
data class Result(val id: String, val title: String, val description: String, val thumbnail: String)
var query = $state("")
var results: List<Result> = $state(listOf())
suspend fun search() {
results = $fetch.get(url = "https://api.example.com/search", params = { q: query })
}
<Column class="w-full h-full">
<TextField bind:value={query} placeholder="Search..." />
<LazyColumn class="gap-12 p-16">
@for (result in results, key = { it.id }) {
<Card onClick={() => $navigate("/item/${result.id}")}>
<Row class="gap-12 p-12">
<Image src={result.thumbnail} class="w-60 h-60" />
<Column class="flex-1">
<Text class="font-medium">{result.title}</Text>
<Text class="text-sm text-muted-foreground">{result.description}</Text>
</Column>
</Row>
</Card>
} empty {
<Column class="w-full items-center pt-60 gap-8">
<Icon name="lucide:search-x" size={48} color="muted-foreground" />
<Text class="text-muted-foreground">No results found</Text>
</Column>
}
</LazyColumn>
</Column> Performance
- Give
@forakeywhenever rows carry their own state or an animation moves them - Keep row layouts simple for smooth scrolling
- Avoid heavy computation inside the loop body — compute it once, above the list