useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data.The list layout renders collection data as a vertical, card-based list. Each row is rendered by a card component — either the built-in CroutonDefaultCard or a custom component you provide via the card prop.
When you set layout="list" on CroutonCollection, each row is rendered as:
<component :is="customCardComponent || CroutonDefaultCard" :item="row" layout="list" :collection="collection" />
The card component receives the row data as the item prop. CroutonDefaultCard uses useDisplayConfig() to automatically map your data fields to title, subtitle, image, and badge slots.
useDisplayConfig(collectionName) resolves display roles from your collection config. When no explicit display config exists, it infers fields by name and type:
| Role | Inferred from (priority order) | Fallback |
|---|---|---|
| title | title, name, label | First string field |
| subtitle | subtitle, description, summary | (skips title field) |
| image | First field of type image or asset | — |
| badge | status, state, category | First field with displayAs: 'badge' |
| description | description, summary, excerpt | (skips fields already used) |
You can also set these explicitly in your collection config:
// app.config.ts
croutonCollections: {
products: {
display: { title: 'name', subtitle: 'brand', image: 'photo', badge: 'status' }
}
}
With standard field names, zero configuration is needed:
<script setup lang="ts">
const { items } = await useCollectionQuery('users')
</script>
<template>
<CroutonCollection
:rows="items"
layout="list"
collection="users"
/>
</template>
CroutonDefaultCard automatically displays the name as title, email as subtitle, and avatar/image as a thumbnail.
Combine list and table layouts using responsive breakpoints:
<template>
<CroutonCollection
:rows="items"
:layout="{
base: 'list',
lg: 'table'
}"
collection="users"
/>
</template>
On mobile, users see a card-style list. On desktop (lg+), it switches to a full table. All Tailwind breakpoints are supported: sm, md, lg, xl, 2xl.
For full control over how each row renders, create a custom card component and pass it via the card prop.
card Prop WorksThe card prop specifies a variant suffix. CroutonCollection resolves it by combining the PascalCase collection name with the variant:
collection="users" + card="Card" resolves UsersCardcollection="shopProducts" + card="ListItem" resolves ShopProductsListItemIf no card prop is set, the default convention is {Collection}Card (e.g., UsersCard). If that component does not exist, CroutonDefaultCard is used.
<!-- components/UsersCard.vue -->
<script setup lang="ts">
interface Props {
item: any
layout: 'list' | 'grid'
collection: string
stateless?: boolean
}
const props = defineProps<Props>()
const crouton = useCrouton()
</script>
<template>
<div class="flex items-center gap-3 w-full">
<img
v-if="item.avatar"
:src="item.avatar"
:alt="item.name"
class="size-10 rounded-full object-cover"
>
<div class="flex-1 min-w-0">
<p class="font-medium truncate">{{ item.name }}</p>
<p class="text-sm text-muted truncate">{{ item.email }}</p>
</div>
<UBadge :color="item.role === 'admin' ? 'primary' : 'neutral'" size="xs">
{{ item.role }}
</UBadge>
<UDropdownMenu
v-if="!stateless"
:items="[
{ label: 'Edit', icon: 'i-lucide-edit', click: () => crouton.open('update', collection, [item.id]) },
{ label: 'Delete', icon: 'i-lucide-trash', color: 'red', click: () => crouton.open('delete', collection, [item.id]) }
]"
>
<UButton icon="i-lucide-more-vertical" variant="ghost" size="sm" />
</UDropdownMenu>
</div>
</template>
Then use it:
<template>
<CroutonCollection
:rows="users"
layout="list"
collection="users"
/>
</template>
Since the component is named UsersCard, it is auto-resolved for the users collection. No card prop needed.
<!-- components/ShopProductsCard.vue -->
<script setup lang="ts">
defineProps<{ item: any; layout: 'list' | 'grid'; collection: string; stateless?: boolean }>()
</script>
<template>
<div class="flex items-center gap-3 w-full">
<div v-if="item.image" class="shrink-0 size-12 rounded overflow-hidden bg-gray-100 dark:bg-gray-800">
<img :src="item.image" :alt="item.name" class="size-full object-cover">
</div>
<div class="flex-1 min-w-0">
<p class="font-medium truncate">{{ item.name }}</p>
<p class="text-sm text-muted truncate">{{ item.description }}</p>
</div>
<span class="font-semibold tabular-nums">${{ item.price?.toFixed(2) }}</span>
<UBadge :color="item.inStock ? 'success' : 'error'" size="xs">
{{ item.inStock ? 'In Stock' : 'Out of Stock' }}
</UBadge>
</div>
</template>
<!-- components/ContactsCard.vue -->
<script setup lang="ts">
defineProps<{ item: any; layout: 'list' | 'grid'; collection: string; stateless?: boolean }>()
const callContact = (phone: string) => window.location.href = `tel:${phone}`
const emailContact = (email: string) => window.location.href = `mailto:${email}`
</script>
<template>
<div class="flex items-center gap-3 w-full">
<img
v-if="item.profileImage"
:src="item.profileImage"
:alt="item.name"
class="size-10 rounded-full object-cover"
>
<div class="flex-1 min-w-0">
<p class="font-medium truncate">{{ item.name }}</p>
<p class="text-sm text-muted truncate">{{ item.email }}</p>
</div>
<div class="flex items-center gap-1">
<UButton
v-if="item.phone"
icon="i-lucide-phone"
variant="ghost"
size="sm"
@click="callContact(item.phone)"
/>
<UButton
icon="i-lucide-mail"
variant="ghost"
size="sm"
@click="emailContact(item.email)"
/>
</div>
</div>
</template>
Field naming -- Use standard names (name, title, email, description, status, image) for automatic detection. Unusual names like usr_nm will not be auto-detected.
Custom cards -- Keep list cards horizontally compact. Use truncate on text, icon-only buttons for actions, and dropdown menus when you have more than two actions.
Responsive layouts -- Always test on actual mobile widths. The { base: 'list', lg: 'table' } pattern covers most use cases.
Touch targets -- Ensure action buttons are at least 44x44px on mobile.
Items show IDs instead of titles: Your data fields are not matching the auto-detection heuristics. Either rename fields to standard names, or set explicit display config in your collection.
Images not showing: CroutonDefaultCard looks for an image field mapped via useDisplayConfig(). Ensure your field is typed as image or asset in the collection schema, or set display.image explicitly.
Custom card not picked up: Verify the component name matches the pattern {PascalCollection}Card (e.g., ShopProductsCard for collection shopProducts). The component must be auto-importable (in components/ directory).