v0-adaptive-table

Each example has its own Desktop/Mobile switch, so you can compare both layouts without resizing the window — real resizing works too, the switch just forces the breakpoint for convenience.

Usage #

The same shape as the classic Vuetify data-table example — a plain field list, no slots, no options. Resize the window to see it become cards, then open the code below to see what produced it.

View
Frozen Yogurt1596244
Ice cream sandwich2379374.3
Eclair26216236
Cupcake3053.7674.3
Gingerbread35616493.9
Jelly bean3750940
Lollipop3920.2980
Honeycomb4083.2876.5
</> View code
const table = createDataTable<Dessert>()
table.onboard(desserts.map(dessert => ({ id: dessert.id, value: dessert })))

const fields: AdaptiveField<Dessert>[] = [
  { key: 'name', label: 'Dessert', mobile: { role: 'title' } },
  { key: 'calories', label: 'Calories', type: 'number', align: 'end' },
  { key: 'fat', label: 'Fat (g)', type: 'number', align: 'end' },
  { key: 'carbs', label: 'Carbs (g)', type: 'number', align: 'end' },
  { key: 'protein', label: 'Protein (g)', type: 'number', align: 'end' },
]

<AdaptiveTable :table="table" :fields="fields" item-key="id" />

Hiding columns #

A field can be dropped from the table, from the card, or from both — table and card visibility are independent.

View
Placed on
Ada LovelacePaid€128.50Jul 1, 2026
Alan TuringPending€64.00Jul 3, 2026
Grace HopperPaid€212.75Jul 5, 2026
Margaret HamiltonRefunded€45.00Jul 8, 2026
</> View code
{
  key: 'placedAt',
  label: 'Placed on',
  type: 'date',
  mobile: { role: 'body' },
}

Card labels #

Card fields render bare by default, since the layout already gives them meaning. Turn on a label when a bare value would be ambiguous.

View
Total
Ada LovelacePaid€128.50Jul 1, 2026
Alan TuringPending€64.00Jul 3, 2026
Grace HopperPaid€212.75Jul 5, 2026
Margaret HamiltonRefunded€45.00Jul 8, 2026
</> View code
{
  key: 'total',
  label: 'Total',
  type: 'currency',
  mobile: { role: 'body' },
}

New line #

Body fields wrap onto a shared line by default. Force one onto its own line when it needs more room or reads better on its own.

View
Status
Ada LovelacePaid€128.50Jul 1, 2026
Alan TuringPending€64.00Jul 3, 2026
Grace HopperPaid€212.75Jul 5, 2026
Margaret HamiltonRefunded€45.00Jul 8, 2026
</> View code
{
  key: 'status',
  label: 'Status',
  mobile: { role: 'body' },
}

Mobile layout #

Two mobile layouts share the same field configuration: stacked cards, or a collapsed list where title and subtitle stay visible as a summary row and body fields only render once a row is expanded — useful when reviewers want the density of a table without giving up the card's readability.

View
Mobile layout
</> View code
<AdaptiveTable
  :table="table"
  :fields="fields"
  item-key="id"
  mobile-mode="list"
/>

Complete example #

Everything at once, on a denser dataset: selection with shift-click ranges, pagination, per-row expand, a masked password field with reveal/copy, per-field toggles for every mobile option, gauges, a sparkline, and a chip list hidden from the desktop table via hideOnDesktop but shown on the card.

View

0 selected —

Columns — visible on
TableCardCard labelNew line
RolesAgenciesTemp. passwordCPUMemoryDiskNetworkLogin activity
user1@example.comAdministratorSupportLyonNantesActiveJul 1, 2026, 8:00 AM••••••••
25%
29%
33%
user2@example.comTechnicianReaderParisMarseilleActiveJul 2, 2026, 9:00 AM••••••••
53%
73%
85%
user3@example.comAdministratorSupportLyonNantesPending validationJul 3, 2026, 10:00 AM••••••••
81%
17%
37%
user4@example.comTechnicianReaderParisMarseilleDisabledJul 4, 2026, 11:00 AM••••••••
9%
61%
89%
user5@example.comAdministratorSupportLyonNantesActiveJul 5, 2026, 12:00 PM••••••••
37%
5%
40%
user6@example.comTechnicianReaderParisMarseilleActiveJul 6, 2026, 1:00 PM••••••••
65%
48%
92%
user7@example.comAdministratorSupportLyonNantesPending validationJul 7, 2026, 2:00 PM••••••••
93%
92%
44%
user8@example.comTechnicianReaderParisMarseilleDisabledJul 8, 2026, 3:00 PM••••••••
20%
36%
96%
user9@example.comAdministratorSupportLyonNantesActiveJul 9, 2026, 4:00 PM••••••••
48%
80%
48%
user10@example.comTechnicianReaderParisMarseilleActiveJul 10, 2026, 5:00 PM••••••••
76%
24%
100%
</> View code
const fields: AdaptiveField<User>[] = [
  { key: 'name', label: 'Name', mobile: { role: 'title' } },
  { key: 'email', label: 'Email', mobile: { role: 'subtitle' } },
  { key: 'stateLabel', label: 'State', type: 'chip' },
  { key: 'temporaryPassword', label: 'Temp. password', type: 'password', sortable: false, filterable: false },
  { key: 'cpuPercent', label: 'CPU', sortable: false, mobile: { role: 'body', block: true } },
  { key: 'networkSamples', label: 'Network', sortable: false, mobile: { role: 'body', block: true } },
]

<AdaptiveTable :table="table" :fields="fields" item-key="id" selectable>
  <template #cell-temporaryPassword="{ item, formatted }">
    {{ revealed ? item.temporaryPassword : formatted }}
    <button @click="toggleReveal(item.id)">Show / hide</button>
  </template>

  <template #cell-cpuPercent="{ value }">
    <v-progress-linear :model-value="value" />
  </template>

  <template #cell-networkSamples="{ value }">
    <v-sparkline :model-value="value" type="trend" />
  </template>

  <template #actions="{ item, isMobile }">
    <button @click="editUser(item)">{{ isMobile ? 'Edit' : '✎' }}</button>
  </template>
</AdaptiveTable>

Design system #

Same data, same AdaptiveTable — this time the look comes entirely from Vuetify components dropped into the cell-/card-/actions slots instead of plain HTML.

View
AL
Ada LovelaceEngineering leadada@example.com
active
€78,000.00Mar 14, 2021
AT
Alan TuringPrincipal engineeralan@example.com
onboarding
€92,000.00Jul 1, 2026
GH
Grace HopperCompiler engineergrace@example.com
active
€88,000.00Nov 2, 2019
MH
Margaret HamiltonSystems architectmargaret@example.com
offboarded
€95,000.00Jun 19, 2017
</> View code
<AdaptiveTable :table="table" :fields="fields" item-key="id">
  <template #cell-status="{ item, formatted }">
    <v-chip :color="statusColor[item.status]" size="small" variant="flat">{{ formatted }}</v-chip>
  </template>

  <template #cell-remote="{ item }">
    <v-icon :icon="item.remote ? 'mdi-check-circle' : 'mdi-close-circle-outline'" />
  </template>

  <template #actions="{ item, isMobile }">
    <v-btn v-if="isMobile" prepend-icon="mdi-pencil" @click="editEmployee(item)">Edit</v-btn>
    <v-btn v-else icon="mdi-pencil" variant="text" aria-label="Edit" @click="editEmployee(item)" />
  </template>
</AdaptiveTable>

API #

The full shape of AdaptiveField and AdaptiveTable — everything used across the examples above.

AdaptiveField

NameTypeDefaultDescription
keykeyof T & string—The property read from each item.
labelstring—Column header on desktop, field label on a card.
type'text' | 'number' | 'currency' | 'date' | 'datetime' | 'boolean' | 'chip' | 'avatar' | 'password'textPicks the default formatter.
format(value: unknown, item: T) => string—Custom formatter, wins over type.
sortablebooleantrueDesktop only, wired to createDataTable sorting.
filterablebooleantrueDesktop only, wired to createDataTable filtering.
align'start' | 'center' | 'end''start'Desktop cell alignment, ignored on a card.
widthstring | number—Desktop column width, ignored on a card.
hideOnDesktopbooleanfalseDrops the field from the table, keeps it on the card.
mobile.role'title' | 'subtitle' | 'body' | 'hidden''body''hidden' drops the field from the card, keeps it on the table.
mobile.ordernumber—Order among fields sharing the same role.
mobile.showLabelbooleanfalsePrefixes the value with the field label on a card.
mobile.blockbooleanfalseGives the field its own line on a card instead of wrapping with the others.

AdaptiveTable props

NameTypeDefaultDescription
tableDataTableContext<T>—The instance returned by createDataTable — required.
fieldsAdaptiveField<T>[]—Drives both the desktop columns and the card layout — required.
itemKeykeyof T & string—Property used as the row/card key.
selectablebooleanfalseRenders a checkbox column on desktop and per card on mobile.
mobileBreakpointBreakpointName | number'sm'Breakpoint below which the mobile layout replaces the table.
mobileMode'card' | 'list''card'Mobile layout: stacked cards, or a collapsed list where each row expands to its body fields.
asstring | Component'div'Root element or component to render as.
renderlessbooleanfalseSkips the root element entirely.

AdaptiveTable slots

NameTypeDefaultDescription
cell-{key}(props: { item, field, value, formatted }) => any—Overrides a desktop cell for that field.
card-{key}(props: { item, field, value, formatted }) => any—Overrides how a field renders inside a card.
actions(props: { item, isMobile }) => any—Renders once per row in both modes.