| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit Card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank Transfer | $350.00 |
| Total | $750.00 | ||
<TableRoot>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="row in invoices" :key="row.invoice">
<TableCell>{{ row.invoice }}</TableCell>
<TableCell>{{ row.status }}</TableCell>
<TableCell>{{ row.method }}</TableCell>
<TableCell class="text-right">{{ row.amount }}</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell :colspan="3">Total</TableCell>
<TableCell class="text-right">$750.00</TableCell>
</TableRow>
</TableFooter>
</TableRoot>
npx @left4code/cosmic-ui-cli@latest add table
This writes the component and everything it imports, then installs the packages it needs. Run npx @left4code/cosmic-ui-cli@latest initfirst if you haven't already (it detects Vue automatically).
No state machine here - a table doesn't need one, it's semantic HTML (table > thead/tbody/tfoot) wrapped in a horizontally scrolling container so wide tables don't blow out the layout on small screens. Vue SFCs can't export multiple components from one file, so Table is a folder instead of a single file — copy all of it into your project.
<script setup lang="ts">
import { twMerge } from "tailwind-merge";
// See the Breadcrumb docs page: without this, Astro's slot="preview" prop
// (used to route this island into a named slot) falls through onto this
// root DOM node inconsistently between SSR and hydration, and Vue flags it
// as a mismatch on every demo page.
defineOptions({ inheritAttrs: false });
const { class: className } = defineProps<{ class?: string }>();
</script>
<template>
<div class="relative w-full overflow-x-auto">
<table :class="twMerge(['w-full border-collapse', className])">
<slot />
</table>
</div>
</template>
<script setup lang="ts">
import { twMerge } from "tailwind-merge";
const { class: className } = defineProps<{ class?: string }>();
</script>
<template>
<thead :class="twMerge(['border-b border-primary/30', className])">
<slot />
</thead>
</template>
<script setup lang="ts">
import { twMerge } from "tailwind-merge";
const { class: className } = defineProps<{ class?: string }>();
</script>
<template>
<tbody :class="twMerge(['[&_tr:last-child]:border-0', className])">
<slot />
</tbody>
</template>
<script setup lang="ts">
import { twMerge } from "tailwind-merge";
const { class: className } = defineProps<{ class?: string }>();
</script>
<template>
<tfoot :class="twMerge(['border-t border-primary/30 font-medium', className])">
<slot />
</tfoot>
</template>
<script setup lang="ts">
import { twMerge } from "tailwind-merge";
const { class: className } = defineProps<{ class?: string }>();
</script>
<template>
<tr :class="twMerge(['border-b border-primary/10 hover:bg-primary/5', className])">
<slot />
</tr>
</template>
<script setup lang="ts">
import { twMerge } from "tailwind-merge";
const { class: className } = defineProps<{ class?: string }>();
</script>
<template>
<th :class="twMerge(['h-10 px-4 text-left align-middle font-medium text-foreground/70', className])">
<slot />
</th>
</template>
<script setup lang="ts">
import { twMerge } from "tailwind-merge";
const { class: className, colspan } = defineProps<{ class?: string; colspan?: number }>();
</script>
<template>
<td :colspan="colspan" :class="twMerge(['p-4 align-middle', className])">
<slot />
</td>
</template>
<script setup lang="ts">
import { twMerge } from "tailwind-merge";
const { class: className } = defineProps<{ class?: string }>();
</script>
<template>
<caption :class="twMerge(['mt-4 opacity-70', className])">
<slot />
</caption>
</template>
export { default as TableRoot } from "./TableRoot.vue";
export { default as TableHeader } from "./TableHeader.vue";
export { default as TableBody } from "./TableBody.vue";
export { default as TableFooter } from "./TableFooter.vue";
export { default as TableRow } from "./TableRow.vue";
export { default as TableHead } from "./TableHead.vue";
export { default as TableCell } from "./TableCell.vue";
export { default as TableCaption } from "./TableCaption.vue";
Update the import paths to match your project setup.
import {
TableRoot,
TableHeader,
TableBody,
TableFooter,
TableRow,
TableHead,
TableCell,
TableCaption,
} from "@/components/ui/table";
<TableRoot>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Ada</TableCell>
<TableCell>Engineer</TableCell>
</TableRow>
</TableBody>
</TableRoot>