Card View
A block for visualizing data in a grid of cards.
Usage
<template>
<MeCardView
v-model:card-selection="cardSelection"
v-model:card-favorite="cardFavorite"
:data="data"
selectable
favorite
>
<template #card-extra-content="{ card }">
<div class="flex flex-col gap-y-2 text-sm">
<div class="flex items-center gap-x-1">
<span class="text-muted">By:</span>
<span class="text-toned font-medium truncate">
{{ itemDetailsById[card.id]?.supplier }}
</span>
</div>
<div class="flex flex-nowrap gap-x-2 overflow-hidden">
<UBadge
v-for="tag in itemDetailsById[card.id]?.tags ?? []"
:key="tag"
color="neutral"
variant="soft"
size="sm"
:label="tag"
class="truncate"
/>
</div>
</div>
</template>
</MeCardView>
</template>
<script setup>
const cardSelection = ref({
'card-1': true,
'card-2': true
})
const cardFavorite = ref({
'card-2': true,
'card-3': true
})
const itemDetailsById = {
'card-1': {
supplier: 'MAXEL MATERIALS ELECTRICS',
tags: ['Abbott currency EUR', '+7']
},
'card-2': {
supplier: 'MAXEL MATERIALS ELECTRICS',
tags: ['Abbott currency EUR', '+7']
},
'card-3': {
supplier: 'MAXEL MATERIALS ELECTRICS',
tags: ['Abbott currency EUR', '+7']
},
'card-4': {
supplier: 'MAXEL MATERIALS ELECTRICS',
tags: ['Abbott currency EUR', '+7']
}
}
const data = [
{
id: 'card-1',
title: 'USD 32,00 - USD 50,00 / UNIT',
description: '000000 - Item name with a long description example.',
image: { src: 'https://picsum.photos/seed/card-1/252/172', alt: 'Product image' },
badges: [
{ icon: 'i-lucide-file-check', color: 'success', description: 'Approved supplier' },
{ icon: 'i-lucide-shopping-basket', color: 'primary', description: 'Currency EUR' }
],
cart: { modelValue: 0, min: 0, onClick: (_cardId, _value) => null }
},
{
id: 'card-2',
title: 'USD 32,00 - USD 50,00 / UNIT',
description: '000000 - Item name with a long description example.',
image: { src: 'https://picsum.photos/seed/card-2/252/172', alt: 'Product image' },
badges: [
{ icon: 'i-lucide-file-check', color: 'success', description: 'Approved supplier' },
{ icon: 'i-lucide-shopping-basket', color: 'primary', description: 'Currency EUR' }
],
cart: { modelValue: 2, min: 0, onClick: (_cardId, _value) => null }
},
{
id: 'card-3',
title: 'USD 39,00 - USD 59,00 / UNIT',
description: '000001 - Item name with a long description example.',
image: { src: 'https://picsum.photos/seed/card-3/252/172', alt: 'Product image' },
badges: [
{ icon: 'i-lucide-file-check', color: 'success', description: 'Approved supplier' },
{ icon: 'i-lucide-shopping-basket', color: 'primary', description: 'Currency EUR' }
],
cart: { modelValue: 1, min: 0, onClick: (_cardId, _value) => null }
},
{
id: 'card-4',
title: 'USD 39,00 - USD 59,00 / UNIT',
description: '000001 - Item name with a long description example.',
image: { src: 'https://picsum.photos/seed/card-4/252/172', alt: 'Product image' },
badges: [
{ icon: 'i-lucide-file-check', color: 'success', description: 'Approved supplier' },
{ icon: 'i-lucide-shopping-basket', color: 'primary', description: 'Currency EUR' }
],
cart: { modelValue: 0, min: 0, onClick: (_cardId, _value) => null }
}
]
</script>
The cards data objects are defined as follows:
type InputCartProps = {
disabled?: boolean
incrementDisabled?: boolean
decrementDisabled?: boolean
cartDisabled?: boolean
min?: number
max?: number
modelValue?: number
}
type CardViewItemData = {
id: string
selectionLabel?: string
image?: { src: string, alt?: string }
badges?: {
icon: string
color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"
description?: string
}[]
title?: string
description?: string
cart?: InputCartProps & { onClick: (cardId: string, value: number | undefined) => unknown }
cardHeight?: string
}
Selection and favorite
Use the selectable and favorite props to enable card selection and the favorite button. Bound the cardSelection and cardFavorite props to v-model to control both behaviors.
<template>
<MeCardView
v-model:card-selection="cardSelection"
v-model:card-favorite="cardFavorite"
:data="data"
selectable
favorite
/>
</template>
<script setup>
const cardSelection = ref({ card0: true })
const cardFavorite = ref({ card1: true })
const data = [
{
id: 'card0',
title: 'Card 0',
description: 'Card 0 description',
image: { src: 'https://picsum.photos/252/172', alt: 'Image' },
cart: { onClick: (_cardId, _value) => null },
badges: [{ icon: 'i-lucide-file-check' }]
},
{
id: 'card1',
title: 'Card 1',
description: 'Card 1 description',
image: { src: 'https://picsum.photos/252/172', alt: 'Image' },
cart: { onClick: (_cardId, _value) => null },
badges: [{ icon: 'i-lucide-file-check' }]
}
]
</script>
Header
Use the header slot to add custom content.
Header slot content
<template>
<MeCardView
:data="data"
selectable
favorite
>
<template #header>
Header slot content
</template>
<MeCardView />
</template>
<script setup>
const data = [{
id: 'card',
title: 'Card title',
description: 'Card description',
image: { src: 'https://picsum.photos/252/172' },
cart: { modelValue: 1 }
}]
</script>
Cart
Defining the cart field in the card data object will render a InputCart component. The behavior of the cart button is defined by the onClick function.
<template>
<MeCardView
:data="data"
favorite
selectable
/>
</template>
<script setup>
const data = [{
id: 'card',
title: 'Card title',
description: 'Card description',
image: { src: 'https://picsum.photos/252/172' },
cart: { modelValue: 3, onClick: (cardId, value) => console.log(cardId, value) }
}]
</script>
Custom cards
Use the card-header, card-image, card-default, card-extra-content and card-footer slots to customize your card. All slots are scoped with the card data object.
<template>
<MeCardView
:data="data"
selectable
favorite
>
<template #card-header="{ card }">
Card {{ card.id }} header
</template>
<template #card-image="{ card }">
<div class="h-43 bg-accented flex justify-center items-start">
Card {{ card.id }} image
</div>
</template>
<template #card-default="{ card }">
<div class="flex flex-col gap-y-2 mt-4 px-2">
<span class="text-default font-bold text-base truncate">
Card {{ card.id }} title
</span>
<span class="text-muted font-normal text-sm line-clamp-2">
Card {{ card.id }} description
</span>
</div>
</template>
<template #card-extra-content="{ card }">
Card {{ card.id }} extra content
</template>
<template #card-footer="{ card }">
Card {{ card.id }} footer
</template>
</MeCardView>
</template>
<script setup>
const data = [{
id: 'custom',
title: 'Custom',
description: 'Custom',
badges: [
{
icon: 'i-lucide-shopping-basket',
color: 'success',
description: 'Shopping basket'
},
{
icon: 'i-lucide-file-check',
color: 'primary'
}
]
}]
</script>
Empty view
Use the empty prop to customize the view when no data is available. It accepts all props from Empty except for variant, plus a src prop to customize the image. Alternatively, use the empty slot to create your empty view from scratch.
<template>
<MeCardView
:data="[]"
:empty="{
title: 'No data',
description: 'No data available',
actions: [{ label: 'Action A' }, { label: 'Action B' }]
}"
selectable
/>
</template>
API
Props
| Prop | Default | Type |
|---|---|---|
data | CardViewItemData | (() => Promise<CardViewItemData[]>) Data to display | |
selectable | boolean Enables card selection | |
cardSelection | {} | Record<string, boolean> Current selected cards, bound with v-model |
favorite | boolean Enables card favorite | |
cardFavorite | {} | Record<string, boolean> Current favorite cards, bound with v-model |
loading | boolean Displays loading state | |
empty | Omit<EmptyProps, 'variant'> & { src?: string } Empty view configuration. See Empty | |
virtualize | Omit<ScrollAreaVirtualizeOptions, 'gap' | 'lanes' | 'paddingStart' | 'paddingEnd'>Virtualization configuration. Use this option appropriately to improve performance with large datasets. See ScrollArea for more info | |
cardHeight | 448px | String Predefined card height |
Slots
| Slot | Type |
|---|---|
header | {} |
empty | {} |
card-header | CardViewItemData |
card-image | CardViewItemData |
card-default | CardViewItemData |
card-extra-content | CardViewItemData |
card-footer | CardViewItemData |