Customizing Generated Code

Layout Components

Customize how your data displays - Card components, CardMini references, and responsive layouts

Layout Components

Learn how to customize layouts in Nuxt Crouton - from responsive presets to custom Card components and CardMini reference displays.

For general layout concepts and Nuxt UI layout components, see the Nuxt UI Layout documentation.

Responsive Layout Presets

Nuxt Crouton provides responsive layout presets that automatically adapt your data views to different screen sizes.

Available Presets

responsive (Default):

  • Base (mobile): List layout
  • md (tablet): Grid layout
  • lg (desktop): Table layout
  • Best for: General-purpose data that needs to work on all devices

mobile-friendly:

  • Base (mobile): List layout
  • lg (desktop): Table layout
  • Best for: Admin interfaces occasionally accessed on mobile

compact:

  • Base (mobile): List layout
  • xl (large desktop): Table layout
  • Best for: Data-dense applications optimized for large screens

tree-default:

  • All breakpoints: Tree layout
  • Best for: Hierarchical data with parent-child relationships

Usage

<script setup lang="ts">
const { items } = await useCollectionQuery('shopProducts')
const { columns } = useShopProducts()
</script>

<template>
  <!-- Responsive preset: list on mobile, table on desktop -->
  <CroutonList
    :rows="items"
    :columns="columns"
    layout="responsive"
  />
</template>

Custom Responsive Layouts

Define your own responsive behavior:

<template>
  <CroutonList
    :rows="items"
    :columns="columns"
    :layout="{
      base: 'list',    // Mobile
      sm: 'list',      // Small tablets
      md: 'list',      // Tablets
      lg: 'table',     // Desktops
      xl: 'table',     // Large desktops
      '2xl': 'table'   // Extra large
    }"
  />
</template>

Breakpoints

Nuxt Crouton uses Tailwind breakpoints:

  • base: < 640px
  • sm: ≥ 640px
  • md: ≥ 768px
  • lg: ≥ 1024px
  • xl: ≥ 1280px
  • 2xl: ≥ 1536px

Why List Layout for Mobile?

All presets use list layout for mobile devices because it's specifically optimized for small screens and touch interfaces:

  • Touch-friendly - Large tap targets optimized for thumbs
  • Vertical scrolling - Natural scrolling pattern for mobile
  • Compact display - Essential information without horizontal scrolling
  • Automatic field detection - Works with standard field names (name, email, avatar)

Card Components

Card components provide complete control over how collection items render in different layouts.

When to Use Custom Cards

Use custom Card components when:

  • Default rendering doesn't match your design
  • You need layout-specific styling
  • You want to show different fields per layout
  • You need custom interactions (click handlers, hover effects)

Stick with defaults when:

  • Basic field display is sufficient
  • You're prototyping or building quickly
  • Your data follows standard conventions

Convention-Based Discovery

Nuxt Crouton automatically discovers Card components using naming conventions:

Collection: "bookings"
Component name: "BookingsCard"

File location: layers/bookings/app/components/Card.vue
Auto-registered as: BookingsCard

Card Variants with the card Prop

You can specify which card variant to use for any layout via the card prop on CroutonCollection or CroutonList:

<template>
  <!-- Default: uses {Collection}Card with layout prop -->
  <CroutonCollection layout="tree" />
  <CroutonCollection layout="list" />

  <!-- Specify card variant -->
  <CroutonCollection layout="tree" card="CardSmall" />
  <CroutonCollection layout="tree" card="CardMini" />
  <CroutonCollection layout="list" card="CardMini" />
</template>

Card Resolution Logic:

  • card="CardSmall"{Collection}CardSmall (e.g., BookingsCardSmall)
  • card="CardMini"{Collection}CardMini
  • No card prop → {Collection}Card with layout prop (default behavior)

This is particularly useful for tree layouts where you might want a compact card variant instead of the full card component:

<template>
  <!-- Tree layout with compact card variant -->
  <CroutonCollection
    layout="tree"
    collection="pages"
    card="CardTree"
  />
</template>

Create the variant component in your collection layer:

layers/pages/app/components/
  ├── Card.vue       # Default card (list, grid, cards layouts)
  └── CardTree.vue   # Tree-specific compact card

Important for Tree Layout: When using a custom card in tree layout, the card replaces only the content area (icon, label, badge) - NOT the drag handle, expand button, or actions menu. This preserves all tree interaction functionality:

[drag handle] [expand btn] | [CARD CONTENT] | [actions menu]
                           └─── replaced ───┘

Props Interface

interface CardProps {
  item: any                                    // The data object
  layout: 'list' | 'grid' | 'cards' | 'tree'  // Current layout context
  collection: string                           // Collection name
  pending?: boolean                            // Loading state
  error?: any                                  // Error state
}

Basic Example

layers/bookings/app/components/Card.vue
<script setup lang="ts">
interface Props {
  item: any
  layout: 'list' | 'grid' | 'cards'
  collection: string
}

const props = defineProps<Props>()
</script>

<template>
  <!-- List: horizontal row -->
  <div v-if="layout === 'list'" class="flex items-center justify-between">
    <span>{{ item.customerName }}</span>
    <UBadge>{{ item.status }}</UBadge>
  </div>

  <!-- Grid: compact card -->
  <UCard v-else-if="layout === 'grid'">
    <h3>{{ item.customerName }}</h3>
    <p class="text-sm">{{ item.date }}</p>
  </UCard>

  <!-- Cards: detailed card -->
  <UCard v-else-if="layout === 'cards'">
    <template #header>
      <h3 class="text-lg">{{ item.customerName }}</h3>
    </template>
    <div class="space-y-2">
      <p>{{ item.date }} at {{ item.time }}</p>
      <p>{{ item.location }}</p>
    </div>
  </UCard>
</template>

Layout-Specific Rendering

List Layout - Best for scannable rows, mobile-first, quick actions:

<template>
  <div v-if="layout === 'list'" class="flex items-center justify-between gap-3">
    <div class="flex items-center gap-3">
      <UIcon name="i-lucide-calendar" class="text-primary" />
      <div>
        <p class="font-medium">{{ item.customerName }}</p>
        <p class="text-sm text-muted">{{ formatDate(item.date) }}</p>
      </div>
    </div>
    <UBadge :color="getStatusColor(item.status)">
      {{ item.status }}
    </UBadge>
  </div>
</template>

Grid Layout - Best for visual browsing, image-heavy content:

<template>
  <UCard
    v-else-if="layout === 'grid'"
    class="cursor-pointer hover:shadow-lg transition"
    @click="handleClick"
  >
    <template #header>
      <div class="flex items-start justify-between">
        <h3 class="font-semibold truncate">{{ item.customerName }}</h3>
        <UBadge size="sm" :color="getStatusColor(item.status)">
          {{ item.status }}
        </UBadge>
      </div>
    </template>

    <div class="space-y-2 text-sm">
      <div class="flex items-center gap-2">
        <UIcon name="i-lucide-calendar" class="text-muted" />
        <span>{{ formatDate(item.date) }}</span>
      </div>
      <div class="flex items-center gap-2">
        <UIcon name="i-lucide-clock" class="text-muted" />
        <span>{{ item.time }}</span>
      </div>
    </div>
  </UCard>
</template>

Cards Layout - Best for detailed information, dashboard widgets:

<template>
  <UCard
    v-else-if="layout === 'cards'"
    class="cursor-pointer hover:shadow-xl transition"
  >
    <template #header>
      <div class="flex items-start gap-3">
        <UAvatar :alt="item.customerName" size="lg" />
        <div class="flex-1">
          <h3 class="font-semibold text-lg">{{ item.customerName }}</h3>
          <p class="text-sm text-muted">{{ item.customerEmail }}</p>
        </div>
        <UBadge size="lg" :color="getStatusColor(item.status)">
          {{ item.status }}
        </UBadge>
      </div>
    </template>

    <div class="space-y-3">
      <div>
        <div class="flex items-center gap-2 text-sm font-medium mb-1">
          <UIcon name="i-lucide-calendar" />
          <span>Date & Time</span>
        </div>
        <p class="text-sm text-muted ml-6">
          {{ formatDate(item.date) }} at {{ item.time }}
        </p>
      </div>

      <div v-if="item.location">
        <div class="flex items-center gap-2 text-sm font-medium mb-1">
          <UIcon name="i-lucide-map-pin" />
          <span>Location</span>
        </div>
        <p class="text-sm text-muted ml-6">{{ item.location }}</p>
      </div>
    </div>

    <template #footer>
      <div class="flex justify-between items-center">
        <span class="text-xs text-muted">
          Created {{ formatRelative(item.createdAt) }}
        </span>
        <div class="flex gap-2">
          <UButton size="xs" variant="ghost" @click.stop="handleEdit">
            Edit
          </UButton>
          <UButton size="xs" @click.stop="handleViewDetails">
            View Details
          </UButton>
        </div>
      </div>
    </template>
  </UCard>
</template>

CardMini Components

CardMini is a compact display component that shows related entities in tables and forms. It's a completely separate system from Card components.

What is CardMini?

When you have reference fields (relationships to other collections), CardMini automatically displays a preview of the referenced item:

  • A booking referencing a user → Shows user's name
  • A product referencing a category → Shows category name
  • An order referencing a customer → Shows customer details

Default Behavior

By default, CardMini displays:

  • The referenced item's title field
  • A loading skeleton while fetching
  • Mini action buttons on hover (update)
  • Error state if item fails to load

Creating Custom CardMini Components

Customize the display for specific collections using convention-based discovery:

Internal Collections:

collections/locations/app/components/
  └── CardMini.vue  ← Create this file

External Collections:

app/components/
  └── UsersCardMini.vue  ← For :users collection

Props Contract

<script setup lang="ts">
const props = defineProps<{
  item: any | null          // The fetched item data
  pending: boolean          // Loading state
  error: any | null         // Error state
  id: string                // Item ID
  collection: string        // Collection name
  refresh: () => Promise<void>  // Refetch function
}>()
</script>

Examples

Basic Custom CardMini:

<script setup lang="ts">
const props = defineProps<{
  item: any
  pending: boolean
  error: any
  id: string
  collection: string
  refresh: () => Promise<void>
}>()

const { open } = useCrouton()
</script>

<template>
  <div class="group relative">
    <USkeleton v-if="pending" class="h-16 w-full rounded-md" />

    <div v-else-if="item" class="border rounded-md p-3 bg-white dark:bg-gray-800">
      <div class="flex items-start gap-3">
        <UIcon name="i-heroicons-map-pin" class="text-blue-500 mt-1" />
        <div class="flex-1">
          <div class="font-medium text-sm">{{ item.name }}</div>
          <div class="text-xs text-gray-500">{{ item.address }}</div>
          <UBadge v-if="item.active" color="green" size="xs" class="mt-1">
            Active
          </UBadge>
        </div>
      </div>
    </div>

    <div v-else-if="error" class="text-red-500 text-xs p-2 border border-red-200 rounded-md">
      Failed to load location
    </div>

    <CroutonMiniButtons
      v-if="item"
      update
      @update="open('update', collection, [id])"
      class="absolute -top-1 right-2 transition delay-150 duration-300 ease-in-out group-hover:-translate-y-6 group-hover:scale-110"
    />
  </div>
</template>

User CardMini with Avatar:

<script setup lang="ts">
const props = defineProps<{
  item: any
  pending: boolean
  error: any
  id: string
  collection: string
  refresh: () => Promise<void>
}>()

const { open } = useCrouton()

const userInitials = computed(() => {
  if (!props.item?.full_name) return '?'
  return props.item.full_name
    .split(' ')
    .map((n: string) => n[0])
    .join('')
    .toUpperCase()
})
</script>

<template>
  <div class="group relative">
    <USkeleton v-if="pending" class="h-12 w-full rounded-lg" />

    <div v-else-if="item" class="flex items-center gap-3 p-2 border rounded-lg bg-white dark:bg-gray-800">
      <!-- Avatar -->
      <div class="flex-shrink-0">
        <img
          v-if="item.avatar_url"
          :src="item.avatar_url"
          :alt="item.full_name"
          class="w-10 h-10 rounded-full"
        />
        <div v-else class="w-10 h-10 rounded-full bg-blue-500 text-white flex items-center justify-center font-medium text-sm">
          {{ userInitials }}
        </div>
      </div>

      <!-- User Info -->
      <div class="flex-1 min-w-0">
        <div class="font-medium text-sm truncate">{{ item.full_name }}</div>
        <div class="text-xs text-gray-500 truncate">{{ item.email }}</div>
      </div>

      <UBadge v-if="item.is_active" color="green" size="xs">
        Active
      </UBadge>
    </div>

    <div v-else-if="error" class="text-red-500 text-xs p-2">
      User not found
    </div>

    <CroutonMiniButtons
      v-if="item"
      update
      @update="open('update', collection, [id])"
      class="absolute -top-1 right-2 transition delay-150 duration-300 ease-in-out group-hover:-translate-y-6 group-hover:scale-110"
    />
  </div>
</template>

Naming Convention

The component name must match this format:

  • Collection: users → Component: UsersCardMini.vue
  • Collection: bookingsLocations → Component: BookingsLocationsCardMini.vue
  • Collection: productCategories → Component: ProductCategoriesCardMini.vue

Rules:

  • Capitalize first letter
  • Add CardMini suffix
  • PascalCase for multi-word collections

Card vs CardMini

Nuxt Crouton has two distinct card systems that serve different purposes:

Card.vue - Layout Rendering

  • Purpose: Display collection items in list/grid/cards layouts
  • Props: item, layout, collection
  • Location: Collection layer (Card.vue)
  • Use case: Main collection views

CardMini - Reference Display

  • Purpose: Display cross-collection references (e.g., show a user in a booking table)
  • Props: id, collection, item, pending, error
  • Location: Collection layer (CardMini.vue) or app (ItemCardMini.vue)
  • Use case: Table cells, form fields, inline references

They are completely separate systems:

<!-- Card: For layout rendering -->
<Card :item="booking" layout="grid" collection="bookings" />

<!-- CardMini: For references -->
<ItemCardMini :id="booking.userId" collection="users" />

Best Practices

Responsive Layouts

✅ DO:

  • Use presets for quick setup (responsive, mobile-friendly, compact)
  • Test layouts on actual devices, not just browser resize
  • Consider mobile-first design principles

❌ DON'T:

  • Mix too many different layouts (confuses users)
  • Forget to test on real mobile devices
  • Change layout types too drastically between breakpoints

Card Components

✅ DO:

  • Handle all three layout types (list, grid, cards)
  • Use computed properties for derived data
  • Handle loading and error states
  • Keep layout logic in the Card component

❌ DON'T:

  • Fetch additional data in Card components (should be pre-loaded)
  • Make Card components too complex
  • Ignore the layout prop
  • Duplicate code across layouts (extract shared logic)

CardMini Components

✅ DO:

  • Keep CardMini components compact (one or two lines of info)
  • Handle loading, error, and empty states
  • Use loading skeletons for better UX
  • Include CroutonMiniButtons for consistency
  • Truncate long text to prevent overflow

❌ DON'T:

  • Make CardMini too large (it's meant to be compact)
  • Fetch additional data (use the provided item prop)
  • Forget to handle pending and error states
  • Remove the mini action buttons
  • Include heavy interactions (modals, forms)

Troubleshooting

Custom Card not showing up

Check file naming:

  • Collection name: bookings
  • Component file: BookingsCard.vue (exact name in Card.vue)
  • Location: collections/bookings/app/components/Card.vue

Check component is auto-imported:

# Restart dev server to refresh auto-imports
pnpm dev

Custom CardMini not showing up

Check file naming:

  • Collection name: users
  • Component file: UsersCardMini.vue (exact name)
  • Location: app/components/ or collections/{name}/app/components/

Layout not changing on resize

The component uses VueUse's useBreakpoints - ensure you're testing with actual browser window resize, not just DevTools responsive mode which can sometimes cache breakpoints.

TypeScript Support

// Card Props
interface CardProps {
  item: any
  layout: 'list' | 'grid' | 'cards' | 'tree'
  collection: string
  pending?: boolean
  error?: any
}

// CardMini Props
interface CardMiniProps {
  item: any | null
  pending: boolean
  error: any | null
  id: string
  collection: string
  refresh: () => Promise<void>
}

// Responsive Layout
type LayoutType = 'table' | 'list' | 'grid' | 'cards' | 'tree'

interface ResponsiveLayout {
  base: LayoutType
  sm?: LayoutType
  md?: LayoutType
  lg?: LayoutType
  xl?: LayoutType
  '2xl'?: LayoutType
}

type LayoutPreset = 'responsive' | 'mobile-friendly' | 'compact' | 'tree-default'

// Collection Props (subset for layouts)
interface CollectionProps {
  layout?: LayoutType | ResponsiveLayout | LayoutPreset
  card?: 'Card' | 'CardMini' | 'CardSmall' | 'CardTree' | string  // Card variant
  // ... other props
}