Blocks

List View

A block for listing of custom items.

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.

Loading preview...
<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 }>

Title

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

RE
123 - RequisiçãoDescrição 1
BRL 100,00
CO
456 - CotaçãoDescrição 2
BRL 200,00
PE
789 - PedidoDescrição 3
BRL 300,00
101 - Mercado EletronicoDescrição 4
BRL 400,00
Loading...
<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
Footer 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

PropDefaultType
activeBehaviorfalseBoolean
Enables row activation
activeRow''String
Sets active row, bound with v-model
dataT[] | () => Promise<T[]>
List rows data
emptyOmit<EmptyProps, 'variant'> & { src?: string }
Empty state configuration. See Empty
loadingfalseBoolean
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
rowSelectionRecord<string, boolean>
Selected rows, bound with v-model
selectablefalseBoolean
Enables row selection

Slots

SlotType
cell{ cell, column, getValue, renderValue, row, table }
empty{}
footer{}
header{ column, header, table }