List View
Create your own custom list while using the block's features.
Usage
MeListView does not provide a default view of your data. Use the cell scoped slot ({ cell, column, getValue, renderValue, row, table }) to compose each row. To enable sticky header and footer behavior, the block must have an explicit height set. See Table for more information.
<template>
<MeListView
v-model:row-selection="rowSelection"
v-model:active-row="activeRow"
v-model:pagination="pagination"
class="h-full"
:data="data"
active-behavior
selectable
>
<template #header>
<div class="flex w-full min-w-0 flex-1 flex-wrap items-center gap-2">
<UButton
color="neutral"
variant="outline"
size="sm"
label="Todos processos"
trailing-icon="i-lucide-chevron-down"
/>
<UButton
color="neutral"
variant="outline"
size="sm"
label="Todo período"
trailing-icon="i-lucide-chevron-down"
/>
<div class="min-w-0 flex-1" />
<UButton
color="neutral"
variant="ghost"
size="sm"
icon="i-lucide-ellipsis-vertical"
aria-label="More actions"
/>
</div>
</template>
<template #cell="{ row }">
<div class="flex w-full items-stretch gap-0">
<MeForehead
class="min-w-0 flex-1"
banner-variant="text"
:text-banner="{ label: row.original.label, code: row.original.code }"
>
<template #leading>
<p class="font-semibold mb-0.5 flex items-center gap-1 text-base text-default">
<span>{{ row.original.code }} - {{ row.original.name }}</span>
<UBadge
v-if="row.original.badge"
:label="row.original.badge"
variant="subtle"
size="sm"
/>
</p>
<div class="text-muted text-xs flex flex-col gap-1">
<p v-if="row.original.createdBy">
Criado por {{ row.original.createdBy }}
</p>
<p v-if="row.original.createdAt">
Criado em {{ row.original.createdAt }}
</p>
<p v-if="row.original.deadline">
Data Limite para Resposta {{ row.original.deadline }}
</p>
<p v-if="row.original.secondaryInfo">
{{ row.original.secondaryInfo }}
</p>
<p
v-if="row.original.phones && row.original.phones.length"
class="flex items-center gap-1 flex-wrap"
>
<template
v-for="(phone, phoneIndex) in row.original.phones"
:key="`${row.id}-phone-${phoneIndex}`"
>
<span>{{ phone }}</span>
<USeparator
v-if="phoneIndex < row.original.phones.length - 1"
orientation="vertical"
class="h-3"
/>
</template>
<UBadge
v-if="row.original.phoneOverflow"
:label="row.original.phoneOverflow"
variant="subtle"
size="xs"
/>
</p>
<p
v-if="row.original.links && row.original.links.length"
class="flex items-center gap-1 flex-wrap"
>
<template
v-for="(link, linkIndex) in row.original.links"
:key="`${row.id}-link-${linkIndex}`"
>
<a
:href="link.href"
class="text-info"
@click.stop
>{{ link.label }}</a>
<USeparator
v-if="linkIndex < row.original.links.length - 1"
orientation="vertical"
class="h-3"
/>
</template>
</p>
</div>
</template>
<template #trailing>
<div class="flex w-full min-w-0 flex-col gap-2 items-start @2xl:items-end">
<p class="text-base font-semibold text-default">
{{ row.original.price }}
</p>
<div class="flex flex-wrap justify-end gap-1">
<UBadge
v-for="(b, bi) in row.original.statusBadges"
:key="bi"
:label="b.label"
:color="b.color"
variant="subtle"
size="sm"
/>
</div>
<div
v-if="row.original.progress != null"
class="w-full max-w-[136px]"
>
<UProgress :model-value="row.original.progress" />
</div>
<MeForeheadActionBar
see-more
:actions="actions"
:dropdown-items="dropdownItems"
@see-more="activeRow = row.id"
/>
</div>
</template>
</MeForehead>
</div>
</template>
</MeListView>
</template>
<script setup>
const rowSelection = ref({ item2: true })
const pagination = ref({ pageIndex: 0, pageSize: 2 })
const activeRow = ref(undefined)
const actions = [
{ icon: 'i-lucide-heart', onClick: () => {} },
{ icon: 'i-lucide-wand-sparkles', onClick: () => {} }
]
const dropdownItems = [
{ label: 'Duplicate', icon: 'i-lucide-copy', onSelect: () => {} },
{ label: 'Delete', icon: 'i-lucide-trash', onSelect: () => {} }
]
const data = [
{
id: 'item1',
label: 'Requisição',
code: '4356776543',
name: 'Requisição de parafusos',
badge: 'RFI',
createdBy: 'Administrador FAST1',
createdAt: '19/08/2026',
secondaryInfo: 'Categoria RFI - Responsible Buyer: Marcel Lottito',
phones: ['(11) 98329911', '(12) 24563946'],
links: [
{ label: 'john.doe@me.com.br', href: 'mailto:john.doe@me.com.br' },
{ label: 'www.site.com.br', href: 'https://www.site.com.br' }
],
price: 'BRL 38,90',
statusBadges: [
{ label: 'Aprovado', color: 'success' },
{ label: '9 dias restantes', color: 'neutral' }
],
progress: null
},
{
id: 'item2',
label: 'Pré-Pedido',
code: '24729602',
name: 'Mercado Eletrônico SA',
badge: null,
createdAt: '22/07/2025 17:59:30',
secondaryInfo: 'Responsável: Iker Buruaga',
phones: ['(11) 98329911', '(12) 24563946', '(12) 24563946', '(12) 24563946'],
phoneOverflow: '+2',
links: [
{ label: 'john.doe@me.com.br', href: 'mailto:john.doe@me.com.br' },
{ label: 'www.site.com.br', href: 'https://www.site.com.br' }
],
price: 'BRL 444,0000',
statusBadges: [
{ label: 'Em análise da Negociação', color: 'warning' },
{ label: '75% Saldo restante', color: 'warning' }
],
progress: null
},
{
id: 'item3',
label: 'Pedido',
code: 'ME_4600068466',
name: 'Mercado Eletrônico SA',
badge: null,
createdBy: 'Flavio Cabral',
phones: ['(11) 983857412'],
links: [
{ label: 'flavio.cabral@me.com.br', href: 'mailto:flavio.cabral@me.com.br' }
],
price: 'BRL 719,2000',
statusBadges: [
{ label: 'Confirmado', color: 'success' },
{ label: 'Criado em 27/08/2024 09:16:02', color: 'neutral' }
],
progress: null
},
{
id: 'item4',
label: 'Cotação',
code: '1234567891',
name: 'Cotação',
badge: 'Categoria: RFX',
createdBy: 'Administrador FAST1',
createdAt: '24/02/2026',
deadline: '12/11/2025 09:39:44',
links: [
{ label: 'john.doe@me.com.br', href: 'mailto:john.doe@me.com.br' },
{ label: 'www.site.com.br', href: 'https://www.site.com.br' }
],
price: 'BRL 12.450,00',
statusBadges: [
{ label: 'Em análise da Negociação', color: 'warning' },
{ label: 'Vencida', color: 'error' }
],
progress: 72
},
{
id: 'item5',
label: 'Contrato',
code: '14499712',
name: 'Mercado Eletrônico',
badge: null,
secondaryInfo: 'CNPJ: 07.418.787/0001-55',
createdAt: '27/08/2024 09:16:02',
phones: ['(11) 98329911', '(12) 24563946'],
links: [
{ label: 'john.doe@me.com.br', href: 'mailto:john.doe@me.com.br' },
{ label: 'www.site.com.br', href: 'https://www.site.com.br' }
],
price: 'BRL 2.100,00',
statusBadges: [
{ label: '75% Saldo restante', color: 'warning' },
{ label: '9 dias restantes', color: 'neutral' }
],
progress: 38
}
]
watch(rowSelection, () => console.log('Selected rows: ', rowSelection.value))
watch(pagination, () => console.log('Pagination state: ', pagination.value))
watch(activeRow, () => console.log('Active row: ', activeRow.value))
</script>
Data
Use the data prop to create your listing. It could be an array of objects or a function that returns a Promise that resolves with an array.
type ListViewDataRow = {
//...any other properties you may need
id: string,
avatar?: {
alt?: string,
icon?: string,
src?: string,
text?: string
}
}
<template>
<MeListView
:data="provider"
selectable
>
<template #cell="{ row }">
<MeForehead>
<template #leading>
<div class="flex flex-col gap-y-[6px]">
<span class="text-base text-default font-semibold">
{{ row.original.code }} - {{ row.original.name }}
</span>
<span class="text-xs text-muted font-normal">
{{ row.original.description }}
</span>
</div>
</template>
</MeForehead>
</template>
</MeListView>
</template>
<script setup>
function provider() {
const data = [
{
id: '1',
code: '123',
name: 'Requisição',
description: 'Descrição 1'
},
{
id: '2',
code: '456',
name: 'Cotação',
description: 'Descrição 2'
},
{
id: '3',
code: '789',
name: 'Pedido',
description: 'Descrição 3'
}
]
return new Promise((resolve) => {
setTimeout(() => {
resolve(data)
}, 1500)
})
}
</script>
Pagination
Bound pagination with v-model to control the displayed page index and page size. Pagination is provided by TanStack Table, see Table for more information.
type PaginationState = {
pageIndex: number,
pageSize: number
}
123 - RequisiçãoDescrição 1 | |
456 - CotaçãoDescrição 2 | |
<template>
<MeListView
v-model:pagination="pagination"
:data="data"
selectable
>
<template #cell="{ row }">
<MeForehead>
<template #leading>
<div class="flex flex-col gap-y-[6px]">
<span class="text-base text-default font-semibold">
{{ row.original.code }} - {{ row.original.name }}
</span>
<span class="text-xs text-muted font-normal">
{{ row.original.description }}
</span>
</div>
</template>
</MeForehead>
</template>
</MeListView>
</template>
<script setup>
const pagination = ref({ pageIndex: 0, pageSize: 2 })
const data = [
{
id: 'item1',
code: '123',
name: 'Requisição',
description: 'Descrição 1'
},
{
id: 'item2',
code: '456',
name: 'Cotação',
description: 'Descrição 2'
},
{
id: 'item3',
code: '789',
name: 'Pedido',
description: 'Descrição 3'
},
{
id: 'item4',
code: '101',
name: 'Mercado Eletronico',
description: 'Descrição 4'
}
]
</script>
Selected and active rows
Bound rowSelection and activeRow with v-model to control which rows are selected or activated. Selection and activation are only enabled when selectable and active-behavior props are true, respectively.
type RowSelectionState = Record<string, boolean> // { rowId: boolean (whether or not row is selected) }
const activeRow: string // id of the current active row
123 - RequisiçãoDescrição 1 | |
456 - CotaçãoDescrição 2 | |
789 - PedidoDescrição 3 | |
<template>
<MeListView
v-model:active-row="activeRow"
v-model:row-selection="rowSelection"
:data="data"
selectable
active-behavior
>
<template #cell="{ row }">
<MeForehead>
<template #leading>
<div class="flex flex-col gap-y-[6px]">
<span class="text-base text-default font-semibold">
{{ row.original.code }} - {{ row.original.name }}
</span>
<span class="text-xs text-muted font-normal">
{{ row.original.description }}
</span>
</div>
</template>
</MeForehead>
</template>
</MeListView>
</template>
<script setup>
const activeRow = ref('item2')
const rowSelection = ref({ item1: true })
const data = [
{
id: 'item1',
code: '123',
name: 'Requisição',
description: 'Descrição 1'
},
{
id: 'item2',
code: '456',
name: 'Cotação',
description: 'Descrição 2'
},
{
id: 'item3',
code: '789',
name: 'Pedido',
description: 'Descrição 3'
}
]
</script>
Empty
Use the empty prop to customize the empty state when no data is provided. It accepts all props from Empty (except variant), plus a src field to customize the image.
const empty: Omit<EmptyProps, 'variant' & { src?: string }>
TitleDescription | |
<template>
<MeListView
:empty="{
title: 'Title',
description: 'Description',
actions: [{ label: 'Action' }]
}"
selectable
/>
</template>
Preview
Combine MePreview with MeListView to create a custom preview for each list item.
<template>
<MePreview :preview-config="{ src: '/' }">
<MeListView
:data="data"
:pagination="{ pageIndex: 0, pageSize: 4 }"
active-row="item1"
active-behavior
selectable
>
<template #cell="{ row }">
<MeForehead>
<template #leading>
<div class="flex flex-col gap-y-[6px]">
<span class="text-base text-default font-semibold">
{{ row.original.code }} - {{ row.original.name }}
</span>
<span class="text-xs text-muted font-normal">
{{ row.original.description }}
</span>
</div>
</template>
<template #trailing>
<div class="flex flex-col justify-between items-end h-full">
<span class="text-base text-default font-semibold">
{{ row.original.price }}
</span>
<MeForeheadActionBar
:actions="[
{ icon: 'i-lucide-heart' },
{ icon: 'i-lucide-wand-sparkles' }
]"
:dropdown-items="[
{ label: 'Duplicate', icon: 'i-lucide-copy', onSelect: () => {} },
{ label: 'Delete', icon: 'i-lucide-trash', onSelect: () => {} }
]"
see-more
/>
</div>
</template>
</MeForehead>
</template>
</MeListView>
</MePreview>
</template>
<script setup>
const data = [
{
id: 'item1',
avatar: { text: 'RE' },
code: '123',
name: 'Requisição',
description: 'Descrição 1',
price: 'BRL 100,00'
},
{
id: 'item2',
avatar: { text: 'CO' },
code: '456',
name: 'Cotação',
description: 'Descrição 2',
price: 'BRL 200,00'
},
{
id: 'item3',
avatar: { text: 'PE' },
code: '789',
name: 'Pedido',
description: 'Descrição 3',
price: 'BRL 300,00'
},
{
id: 'item4',
avatar: { alt: 'Default avatar' },
code: '101',
name: 'Mercado Eletronico',
description: 'Descrição 4',
price: 'BRL 400,00'
}
]
</script>
Slots
The block provides the header (scoped { column, header, table }), cell (scoped ({ cell, column, getValue, renderValue, row, table })) and footer slots.
Header slot | |
|---|---|
| Row item1 cell slot | |
| Row item2 cell slot | |
| Row item3 cell slot | |
<template>
<MeListView
:data="data"
selectable
>
<template #header>
Header slot
</template>
<template #cell="{ row }">
Row {{ row.original.id }} cell slot
</template>
<template #footer>
Footer slot
</template>
</MeListView>
</template>
<script setup>
const data = [
{
id: 'item1',
code: '123',
name: 'Requisição',
description: 'Descrição 1'
},
{
id: 'item2',
code: '456',
name: 'Cotação',
description: 'Descrição 2'
},
{
id: 'item3',
code: '789',
name: 'Pedido',
description: 'Descrição 3'
}
]
</script>
API
Props
| Prop | Default | Type |
|---|---|---|
activeBehavior | false | Boolean Enables row activation |
activeRow | '' | String Sets active row, bound with v-model |
data | T[] | () => Promise<T[]> List rows data | |
empty | Omit<EmptyProps, 'variant'> & { src?: string } Empty state configuration. See Empty | |
loading | false | Boolean Sets list to loading state |
loadingColor | 'primary' | 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral' Loading bar color |
loadingAnimation | 'carousel' | 'carousel' | 'carousel-inverse' | 'swing' | 'elastic' Loading bar animation |
pagination | { pageIndex: number, pageSize: number } Pagination state, bound with v-model | |
rowSelection | Record<string, boolean> Selected rows, bound with v-model | |
selectable | false | Boolean Enables row selection |
Slots
| Slot | Type |
|---|---|
cell | { cell, column, getValue, renderValue, row, table } |
empty | {} |
footer | {} |
header | { column, header, table } |