Patterns

List Layout

Mobile-optimized list view with automatic field detection and custom actions
Query Examples: For complete 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:

  1. name
  2. title
  3. label
  4. email
  5. username
  6. id

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:

  1. description
  2. email (if name exists)
  3. username (if name exists)
  4. role
  5. createdAt (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:

  1. avatar (object with avatar props)
  2. image (string URL)
  3. avatarUrl (string URL)
  4. 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>
<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_nm or desc_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:

  1. Ensure you're using layout="list" (not layout="table")
  2. Verify the slot name is exactly list-item-actions (with hyphen)
  3. 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
}