Patterns

List Layout

Card-based list view with automatic field detection and custom card components
Query Examples: For complete 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.

How It Works

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.

Automatic Field Detection

useDisplayConfig(collectionName) resolves display roles from your collection config. When no explicit display config exists, it infers fields by name and type:

RoleInferred from (priority order)Fallback
titletitle, name, labelFirst string field
subtitlesubtitle, description, summary(skips title field)
imageFirst field of type image or asset
badgestatus, state, categoryFirst field with displayAs: 'badge'
descriptiondescription, 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' }
  }
}

Basic Usage

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.

Responsive Layouts

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.

Custom Card Components

For full control over how each row renders, create a custom card component and pass it via the card prop.

How the card Prop Works

The card prop specifies a variant suffix. CroutonCollection resolves it by combining the PascalCase collection name with the variant:

  • collection="users" + card="Card" resolves UsersCard
  • collection="shopProducts" + card="ListItem" resolves ShopProductsListItem

If no card prop is set, the default convention is {Collection}Card (e.g., UsersCard). If that component does not exist, CroutonDefaultCard is used.

Example: User Card

<!-- 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.

Example: Product Card

<!-- 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>

Example: Contact Card with Actions

<!-- 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>

Best Practices

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.

Troubleshooting

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).