List Layout
useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data.The list layout provides a mobile-optimized, card-style view for displaying collection data. Unlike the table layout, list view presents data in a vertical, scannable format that's perfect for touch interfaces.
Overview
List layout is ideal for:
- Mobile-first applications - Touch-friendly with large tap targets
- User directories - Profiles with avatars, names, and details
- Contact lists - Names, emails, phone numbers
- Product catalogs - Images, titles, descriptions
- Activity feeds - Timeline-style displays
When to Use List vs Table
Use list layout when:
- Primary use case is mobile/tablet devices
- Data has profile images or avatars
- You need 2-3 key pieces of info per item
- Vertical scrolling is preferred
Use table layout when:
- Working with data-dense information
- Multiple columns need to be compared
- Desktop is the primary interface
- Sorting and filtering multiple fields is important
Automatic Field Mapping
The list layout's most powerful feature is automatic field detection - it intelligently identifies common field names in your data and displays them appropriately, with zero configuration required.
Title Field Detection
The list layout searches for these fields in priority order for the primary display text:
nametitlelabelemailusernameid
Example:
const users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' }
]
// Automatically displays "John Doe" as the title
Subtitle Field Detection
For secondary text displayed below the title:
descriptionemail(ifnameexists)username(ifnameexists)rolecreatedAt(auto-formatted as date)
Example:
const users = [
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' }
]
// Title: "John Doe"
// Subtitle: "john@example.com"
Avatar Field Detection
For profile images or product photos:
avatar(object with avatar props)image(string URL)avatarUrl(string URL)profileImage(string URL)
Avatar as Object:
const users = [
{
name: 'John Doe',
avatar: {
src: '/avatars/john.jpg',
alt: 'John Doe'
}
}
]
Avatar as String:
const products = [
{
name: 'Premium Headphones',
image: '/products/headphones.jpg'
}
]
Basic Usage
Zero Configuration
Thanks to automatic field detection, you can display lists with minimal setup. Query your collection data and pass it to the CroutonList component - it will automatically detect and display name, email, and avatar fields.
With Responsive Breakpoints
Combine list layout with responsive presets:
<template>
<CroutonList
:rows="items"
:layout="{
base: 'list', // Mobile: card-style list
lg: 'table' // Desktop: full table
}"
collection="users"
/>
</template>
Custom List Item Actions
The list-item-actions slot allows you to customize the action buttons that appear on the right side of each list item, replacing or augmenting the default edit/delete actions.
Basic Custom Actions
<script setup lang="ts">
const { items } = await useCollectionQuery('users')
const handleDelete = async (id: string) => {
await useCollectionMutation('users').deleteItems([id])
}
</script>
<template>
<CroutonList
:rows="items"
layout="list"
collection="users"
>
<template #list-item-actions="{ row }">
<UButton
icon="i-lucide-mail"
variant="ghost"
size="sm"
@click="sendEmail(row.email)"
/>
<UButton
icon="i-lucide-trash"
color="red"
variant="ghost"
size="sm"
@click="handleDelete(row.id)"
/>
</template>
</CroutonList>
</template>
Role Selector
<template>
<CroutonList
:rows="users"
layout="list"
collection="users"
>
<template #list-item-actions="{ row }">
<USelect
:model-value="row.role"
:items="['admin', 'member', 'viewer']"
size="sm"
@update:model-value="updateRole(row.id, $event)"
/>
</template>
</CroutonList>
</template>
Dropdown Menu
<template>
<CroutonList
:rows="users"
layout="list"
collection="users"
>
<template #list-item-actions="{ row }">
<UDropdownMenu
:items="[
{ label: 'Edit', icon: 'i-lucide-edit', click: () => handleEdit(row) },
{ label: 'Permissions', icon: 'i-lucide-shield', click: () => handlePermissions(row) },
{ label: 'Delete', icon: 'i-lucide-trash', color: 'red', click: () => handleDelete(row) }
]"
>
<UButton
icon="i-lucide-more-vertical"
variant="ghost"
size="sm"
/>
</UDropdownMenu>
</template>
</CroutonList>
</template>
Real-World Examples
User Management List
<script setup lang="ts">
const { items: users } = await useCollectionQuery('users') // See fundamentals/querying for query patterns
const { columns } = useUsers()
const updateRole = async (userId: string, newRole: string) => {
await useCollectionMutation('users').update(userId, { role: newRole })
}
</script>
<template>
<CroutonList
:rows="users"
:columns="columns"
layout="list"
collection="users"
>
<template #list-item-actions="{ row }">
<USelect
:model-value="row.role"
:items="['admin', 'member', 'viewer']"
size="sm"
@update:model-value="updateRole(row.id, $event)"
/>
<UDropdownMenu
:items="[
{ label: 'Edit Profile', icon: 'i-lucide-edit' },
{ label: 'View Activity', icon: 'i-lucide-activity' },
{ label: 'Remove', icon: 'i-lucide-trash', color: 'red' }
]"
>
<UButton icon="i-lucide-more-vertical" variant="ghost" size="sm" />
</UDropdownMenu>
</template>
</CroutonList>
</template>
Data structure:
// Automatic field mapping in action:
const users = [
{
id: 1,
name: 'John Doe', // → Title
email: 'john@example.com', // → Subtitle
role: 'admin', // → Available in actions
avatar: { src: '/john.jpg' } // → Avatar image
}
]
E-commerce Product List
<script setup lang="ts">
const { items: products } = await useCollectionQuery('shopProducts')
const { columns } = useShopProducts()
const addToCart = (product) => {
// Add to cart logic
}
</script>
<template>
<CroutonList
:rows="products"
:columns="columns"
:layout="{
base: 'list',
md: 'list',
lg: 'table'
}"
collection="shopProducts"
>
<template #list-item-actions="{ row }">
<UBadge v-if="row.inStock" color="green" size="sm">
In Stock
</UBadge>
<UBadge v-else color="red" size="sm">
Out of Stock
</UBadge>
<UButton
size="sm"
:disabled="!row.inStock"
@click="addToCart(row)"
>
Add to Cart
</UButton>
</template>
</CroutonList>
</template>
Data structure:
const products = [
{
id: 1,
name: 'Premium Headphones', // → Title
description: 'Wireless, noise...', // → Subtitle
image: '/products/headphones.jpg', // → Avatar/Image
price: 299.99,
inStock: true
}
]
Contact List
<script setup lang="ts">
const { items: contacts } = await useCollectionQuery('contacts')
const callContact = (phone: string) => {
window.location.href = `tel:${phone}`
}
const emailContact = (email: string) => {
window.location.href = `mailto:${email}`
}
</script>
<template>
<CroutonList
:rows="contacts"
layout="list"
collection="contacts"
>
<template #list-item-actions="{ row }">
<UButton
v-if="row.phone"
icon="i-lucide-phone"
variant="ghost"
size="sm"
@click="callContact(row.phone)"
/>
<UButton
icon="i-lucide-mail"
variant="ghost"
size="sm"
@click="emailContact(row.email)"
/>
</template>
</CroutonList>
</template>
Data structure:
const contacts = [
{
id: 1,
name: 'Sarah Johnson', // → Title
email: 'sarah@example.com', // → Subtitle
phone: '+1-555-0123',
profileImage: '/sarah.jpg' // → Avatar
}
]
Best Practices
Field Naming Conventions
✅ DO:
- Use standard field names (
name,email,description,avatar) for automatic detection - Provide both title and subtitle fields for better UX
- Include avatar/image URLs for visual appeal
- Keep field names consistent across collections
❌ DON'T:
- Use obscure field names like
usr_nmordesc_txt(won't be auto-detected) - Mix field naming conventions across collections
- Forget to provide fallback fields if primary fields might be missing
Custom Actions
✅ DO:
- Use icon-only buttons for space efficiency
- Keep actions relevant to the item context
- Use color coding (red for delete, etc.)
- Group related actions in dropdown menus
❌ DON'T:
- Add too many action buttons (clutters the UI)
- Use text-only buttons (takes too much space)
- Forget to handle loading states
- Add actions that navigate away without user confirmation
Mobile Optimization
✅ DO:
- Test on actual mobile devices
- Ensure touch targets are at least 44x44px
- Use responsive layout presets
- Consider thumb reach on large phones
❌ DON'T:
- Make action buttons too small
- Add too much text to subtitles
- Use complex interactions that don't work on touch
- Forget about landscape orientation
Troubleshooting
List Items Not Displaying Title/Subtitle
Problem: List items show IDs instead of meaningful text.
Solution: Ensure your data includes recognized field names:
// ❌ Won't auto-detect
const users = [
{ id: 1, full_name: 'John', usr_email: 'john@example.com' }
]
// ✅ Will auto-detect
const users = [
{ id: 1, name: 'John', email: 'john@example.com' }
]
Alternatively, use the columns prop with custom render functions to map your fields.
Avatar Images Not Showing
Problem: Avatars are missing or showing placeholder icons.
Solution: Check your field names match the priority list:
// ✅ These will work:
{ avatar: { src: '/path.jpg' } }
{ image: '/path.jpg' }
{ avatarUrl: '/path.jpg' }
{ profileImage: '/path.jpg' }
// ❌ These won't auto-detect:
{ picture: '/path.jpg' }
{ photo: '/path.jpg' }
Custom Actions Not Appearing
Problem: list-item-actions slot content doesn't show.
Solution:
- Ensure you're using
layout="list"(notlayout="table") - Verify the slot name is exactly
list-item-actions(with hyphen) - Check the row data is being passed correctly
<!-- ❌ Wrong slot name -->
<template #list-actions="{ row }">
<!-- ✅ Correct slot name -->
<template #list-item-actions="{ row }">
Actions Overlapping on Mobile
Problem: Action buttons are cut off or overlapping on small screens.
Solution:
- Use icon-only buttons (no text labels)
- Reduce number of visible actions
- Use a dropdown menu for multiple actions
- Test on actual device widths
<template #list-item-actions="{ row }">
<!-- ❌ Too many buttons -->
<UButton>Edit</UButton>
<UButton>View</UButton>
<UButton>Share</UButton>
<UButton>Delete</UButton>
<!-- ✅ Consolidated in dropdown -->
<UDropdownMenu :items="actions">
<UButton icon="i-lucide-more-vertical" variant="ghost" size="sm" />
</UDropdownMenu>
</template>
TypeScript Support
Full type safety for list layout:
// Field mapping types
interface ListItemFields {
// Title fields (in priority order)
name?: string
title?: string
label?: string
email?: string
username?: string
id: string | number
// Subtitle fields
description?: string
role?: string
createdAt?: Date | string
// Avatar fields
avatar?: { src: string; alt?: string }
image?: string
avatarUrl?: string
profileImage?: string
}
// Custom actions slot
interface ListItemActionsSlot {
row: any // Your data type
}
Related Sections
- Responsive Layouts - Using list layout with breakpoints
- CroutonList Component - Complete API reference
- Custom Components - Building custom list components
- Table Configuration - Comparison with table layout