Layout Components
Layout Components
Learn how to customize layouts in Nuxt Crouton - from responsive presets to custom Card components and CardMini reference displays.
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
cardprop →{Collection}Cardwithlayoutprop (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
<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
titlefield - 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
CardMinisuffix - 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
itemprop) - Forget to handle
pendinganderrorstates - 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/orcollections/{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
}
Related Topics
- Custom Components - Other customization patterns
- Custom Columns - Customizing table columns
- Working with Relations - Working with relationships
- Table Patterns - Table configuration and composition