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.
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.
| Frozen Yogurt | 159 | 6 | 24 | 4 |
| Ice cream sandwich | 237 | 9 | 37 | 4.3 |
| Eclair | 262 | 16 | 23 | 6 |
| Cupcake | 305 | 3.7 | 67 | 4.3 |
| Gingerbread | 356 | 16 | 49 | 3.9 |
| Jelly bean | 375 | 0 | 94 | 0 |
| Lollipop | 392 | 0.2 | 98 | 0 |
| Honeycomb | 408 | 3.2 | 87 | 6.5 |
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" />A field can be dropped from the table, from the card, or from both — table and card visibility are independent.
| Ada Lovelace | Paid | €128.50 | Jul 1, 2026 |
| Alan Turing | Pending | €64.00 | Jul 3, 2026 |
| Grace Hopper | Paid | €212.75 | Jul 5, 2026 |
| Margaret Hamilton | Refunded | €45.00 | Jul 8, 2026 |
{
key: 'placedAt',
label: 'Placed on',
type: 'date',
mobile: { role: 'body' },
}Card fields render bare by default, since the layout already gives them meaning. Turn on a label when a bare value would be ambiguous.
| Ada Lovelace | Paid | €128.50 | Jul 1, 2026 |
| Alan Turing | Pending | €64.00 | Jul 3, 2026 |
| Grace Hopper | Paid | €212.75 | Jul 5, 2026 |
| Margaret Hamilton | Refunded | €45.00 | Jul 8, 2026 |
{
key: 'total',
label: 'Total',
type: 'currency',
mobile: { role: 'body' },
}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.
| Ada Lovelace | Paid | €128.50 | Jul 1, 2026 |
| Alan Turing | Pending | €64.00 | Jul 3, 2026 |
| Grace Hopper | Paid | €212.75 | Jul 5, 2026 |
| Margaret Hamilton | Refunded | €45.00 | Jul 8, 2026 |
{
key: 'status',
label: 'Status',
mobile: { role: 'body' },
} 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.
<AdaptiveTable
:table="table"
:fields="fields"
item-key="id"
mobile-mode="list"
/> 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.
0 selected —
| Roles | Agencies | Temp. password | CPU | Memory | Disk | Network | Login activity | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| user1@example.com | AdministratorSupport | LyonNantes | Active | Jul 1, 2026, 8:00 AM | •••••••• | 25% | 29% | 33% | |||||
| user2@example.com | TechnicianReader | ParisMarseille | Active | Jul 2, 2026, 9:00 AM | •••••••• | 53% | 73% | 85% | |||||
| user3@example.com | AdministratorSupport | LyonNantes | Pending validation | Jul 3, 2026, 10:00 AM | •••••••• | 81% | 17% | 37% | |||||
| user4@example.com | TechnicianReader | ParisMarseille | Disabled | Jul 4, 2026, 11:00 AM | •••••••• | 9% | 61% | 89% | |||||
| user5@example.com | AdministratorSupport | LyonNantes | Active | Jul 5, 2026, 12:00 PM | •••••••• | 37% | 5% | 40% | |||||
| user6@example.com | TechnicianReader | ParisMarseille | Active | Jul 6, 2026, 1:00 PM | •••••••• | 65% | 48% | 92% | |||||
| user7@example.com | AdministratorSupport | LyonNantes | Pending validation | Jul 7, 2026, 2:00 PM | •••••••• | 93% | 92% | 44% | |||||
| user8@example.com | TechnicianReader | ParisMarseille | Disabled | Jul 8, 2026, 3:00 PM | •••••••• | 20% | 36% | 96% | |||||
| user9@example.com | AdministratorSupport | LyonNantes | Active | Jul 9, 2026, 4:00 PM | •••••••• | 48% | 80% | 48% | |||||
| user10@example.com | TechnicianReader | ParisMarseille | Active | Jul 10, 2026, 5:00 PM | •••••••• | 76% | 24% | 100% |
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> Same data, same AdaptiveTable — this time the look comes entirely from Vuetify components dropped into the cell-/card-/actions slots instead of plain HTML.
The full shape of AdaptiveField and AdaptiveTable — everything used across the examples above.
| Name | Type | Default | Description |
|---|---|---|---|
key | keyof T & string | — | The property read from each item. |
label | string | — | Column header on desktop, field label on a card. |
type | 'text' | 'number' | 'currency' | 'date' | 'datetime' | 'boolean' | 'chip' | 'avatar' | 'password' | text | Picks the default formatter. |
format | (value: unknown, item: T) => string | — | Custom formatter, wins over type. |
sortable | boolean | true | Desktop only, wired to createDataTable sorting. |
filterable | boolean | true | Desktop only, wired to createDataTable filtering. |
align | 'start' | 'center' | 'end' | 'start' | Desktop cell alignment, ignored on a card. |
width | string | number | — | Desktop column width, ignored on a card. |
hideOnDesktop | boolean | false | Drops 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.order | number | — | Order among fields sharing the same role. |
mobile.showLabel | boolean | false | Prefixes the value with the field label on a card. |
mobile.block | boolean | false | Gives the field its own line on a card instead of wrapping with the others. |
| Name | Type | Default | Description |
|---|---|---|---|
table | DataTableContext<T> | — | The instance returned by createDataTable — required. |
fields | AdaptiveField<T>[] | — | Drives both the desktop columns and the card layout — required. |
itemKey | keyof T & string | — | Property used as the row/card key. |
selectable | boolean | false | Renders a checkbox column on desktop and per card on mobile. |
mobileBreakpoint | BreakpointName | 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. |
as | string | Component | 'div' | Root element or component to render as. |
renderless | boolean | false | Skips the root element entirely. |
| Name | Type | Default | Description |
|---|---|---|---|
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. |