Table
A plain, styled HTML table. Numbers are right-aligned and use the mono face; the footer holds totals.
import { Card } from "@/components/ui/card";import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow,} from "@/components/ui/table";
const rows = [ { symbol: "NVDAx", name: "NVIDIA", price: "$188.20", weight: "32.0%", day: "+2.41%", up: true, }, { symbol: "MSFTx", name: "Microsoft", price: "$512.10", weight: "28.0%", day: "+0.88%", up: true, }, { symbol: "AAPLx", name: "Apple", price: "$231.45", weight: "24.0%", day: "-0.62%", up: false, }, { symbol: "ANTHRP-pre", name: "Anthropic pre-IPO", price: "$61.30", weight: "16.0%", day: "+4.10%", up: true, },];
export default function Demo() { return ( <Card className="w-full max-w-xl py-0"> <Table> <TableHeader> <TableRow> <TableHead>Token</TableHead> <TableHead numeric>Price</TableHead> <TableHead numeric>Weight</TableHead> <TableHead numeric>24h</TableHead> </TableRow> </TableHeader> <TableBody> {rows.map((r) => ( <TableRow key={r.symbol}> <TableCell> <div className="font-medium">{r.symbol}</div> <div className="text-[11.5px] text-muted-foreground"> {r.name} </div> </TableCell> <TableCell numeric>{r.price}</TableCell> <TableCell numeric>{r.weight}</TableCell> <TableCell numeric trend={r.up ? "up" : "down"}> {r.day} </TableCell> </TableRow> ))} </TableBody> <TableFooter> <TableRow> <TableCell>Total · 4 tokens</TableCell> <TableCell numeric>NAV $0.9998</TableCell> <TableCell numeric>100%</TableCell> <TableCell numeric trend="up"> +0.53% </TableCell> </TableRow> </TableFooter> <TableCaption>Weights as of the last rebalance.</TableCaption> </Table> </Card> );}<script setup lang="ts">import { Card } from "@/components/ui/card";import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow,} from "@/components/ui/table";
const rows = [ { symbol: "NVDAx", name: "NVIDIA", price: "$188.20", weight: "32.0%", day: "+2.41%", up: true }, { symbol: "MSFTx", name: "Microsoft", price: "$512.10", weight: "28.0%", day: "+0.88%", up: true }, { symbol: "AAPLx", name: "Apple", price: "$231.45", weight: "24.0%", day: "-0.62%", up: false }, { symbol: "ANTHRP-pre", name: "Anthropic pre-IPO", price: "$61.30", weight: "16.0%", day: "+4.10%", up: true },];</script>
<template> <Card class="w-full max-w-xl py-0"> <Table> <TableHeader> <TableRow> <TableHead>Token</TableHead> <TableHead numeric>Price</TableHead> <TableHead numeric>Weight</TableHead> <TableHead numeric>24h</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow v-for="r in rows" :key="r.symbol"> <TableCell> <div class="font-medium">{{ r.symbol }}</div> <div class="text-[11.5px] text-muted-foreground">{{ r.name }}</div> </TableCell> <TableCell numeric>{{ r.price }}</TableCell> <TableCell numeric>{{ r.weight }}</TableCell> <TableCell numeric :trend="r.up ? 'up' : 'down'">{{ r.day }}</TableCell> </TableRow> </TableBody> <TableFooter> <TableRow> <TableCell>Total · 4 tokens</TableCell> <TableCell numeric>NAV $0.9998</TableCell> <TableCell numeric>100%</TableCell> <TableCell numeric trend="up">+0.53%</TableCell> </TableRow> </TableFooter> <TableCaption>Weights as of the last rebalance.</TableCaption> </Table> </Card></template><script lang="ts"> import { Card } from "$lib/components/ui/card"; import * as Table from "$lib/components/ui/table";
const rows = [ { symbol: "NVDAx", name: "NVIDIA", price: "$188.20", weight: "32.0%", day: "+2.41%", up: true }, { symbol: "MSFTx", name: "Microsoft", price: "$512.10", weight: "28.0%", day: "+0.88%", up: true }, { symbol: "AAPLx", name: "Apple", price: "$231.45", weight: "24.0%", day: "-0.62%", up: false }, { symbol: "ANTHRP-pre", name: "Anthropic pre-IPO", price: "$61.30", weight: "16.0%", day: "+4.10%", up: true }, ];</script>
<Card class="w-full max-w-xl py-0"> <Table.Root> <Table.Header> <Table.Row> <Table.Head>Token</Table.Head> <Table.Head numeric>Price</Table.Head> <Table.Head numeric>Weight</Table.Head> <Table.Head numeric>24h</Table.Head> </Table.Row> </Table.Header> <Table.Body> {#each rows as r (r.symbol)} <Table.Row> <Table.Cell> <div class="font-medium">{r.symbol}</div> <div class="text-[11.5px] text-muted-foreground">{r.name}</div> </Table.Cell> <Table.Cell numeric>{r.price}</Table.Cell> <Table.Cell numeric>{r.weight}</Table.Cell> <Table.Cell numeric trend={r.up ? "up" : "down"}>{r.day}</Table.Cell> </Table.Row> {/each} </Table.Body> <Table.Footer> <Table.Row> <Table.Cell>Total · 4 tokens</Table.Cell> <Table.Cell numeric>NAV $0.9998</Table.Cell> <Table.Cell numeric>100%</Table.Cell> <Table.Cell numeric trend="up">+0.53%</Table.Cell> </Table.Row> </Table.Footer> <Table.Caption>Weights as of the last rebalance.</Table.Caption> </Table.Root></Card>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/tablepnpm dlx shadcn@latest add @edmi-ui/tableyarn dlx shadcn@latest add @edmi-ui/tablebunx shadcn@latest add @edmi-ui/tablePick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/tablepnpm dlx shadcn-vue@latest add @edmi-ui/tableyarn dlx shadcn-vue@latest add @edmi-ui/tablebunx shadcn-vue@latest add @edmi-ui/tablePick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/table.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/table.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/table.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/table.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { Card } from "@/components/ui/card";import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow,} from "@/components/ui/table";
<Card className="w-full max-w-xl py-0"> <Table> <TableHeader> <TableRow> <TableHead>Token</TableHead> <TableHead numeric>Price</TableHead> <TableHead numeric>Weight</TableHead> <TableHead numeric>24h</TableHead> </TableRow> </TableHeader> <TableBody> </TableBody> <TableFooter> <TableRow> <TableCell>Total · 4 tokens</TableCell> <TableCell numeric>NAV $0.9998</TableCell> <TableCell numeric>100%</TableCell> <TableCell numeric trend="up"> +0.53% </TableCell> </TableRow> </TableFooter> <TableCaption>Weights as of the last rebalance.</TableCaption> </Table></Card><script setup lang="ts">import { Card } from "@/components/ui/card";import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table";</script>
<template> <Card class="w-full max-w-xl py-0"> <Table> <TableHeader> <TableRow> <TableHead>Token</TableHead> <TableHead numeric>Price</TableHead> <TableHead numeric>Weight</TableHead> <TableHead numeric>24h</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow :key="r.symbol"> <TableCell> <div class="font-medium">{{ r.symbol }}</div> <div class="text-[11.5px] text-muted-foreground">{{ r.name }}</div> </TableCell> <TableCell numeric>{{ r.price }}</TableCell> <TableCell numeric>{{ r.weight }}</TableCell> <TableCell numeric :trend="r.up ? 'up' : 'down'">{{ r.day }}</TableCell> </TableRow> </TableBody> <!-- … --> </Table> </Card></template><script lang="ts"> import * as Table from "$lib/components/ui/table"; import { Card } from "$lib/components/ui/card";</script>
<Card class="w-full max-w-xl py-0"> <Table.Root> <Table.Header> <Table.Row> <Table.Head>Token</Table.Head> <Table.Head numeric>Price</Table.Head> <Table.Head numeric>Weight</Table.Head> <Table.Head numeric>24h</Table.Head> </Table.Row> </Table.Header> <Table.Body> {#each rows as r (r.symbol)} <Table.Row> <Table.Cell> <div class="font-medium">{r.symbol}</div> <div class="text-[11.5px] text-muted-foreground">{r.name}</div> </Table.Cell> <Table.Cell numeric>{r.price}</Table.Cell> <Table.Cell numeric>{r.weight}</Table.Cell> <Table.Cell numeric trend={r.up ? "up" : "down"}>{r.day}</Table.Cell> </Table.Row> {/each} </Table.Body> <!-- … --> </Table.Root></Card>Component source
Section titled “Component source”import { cn } from "cn";import type * as React from "react";
function Table({ className, ...props }: React.ComponentProps<"table">) { return ( <div data-slot="table-container" className="relative w-full overflow-x-auto" > <table data-slot="table" className={cn( "w-full caption-bottom border-collapse text-[13.5px]", className, )} {...props} /> </div> );}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { return ( <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} /> );}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { return ( <tbody data-slot="table-body" className={cn("[&_tr:last-child]:border-0", className)} {...props} /> );}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { return ( <tfoot data-slot="table-footer" className={cn( "border-t bg-muted font-semibold [&>tr]:last:border-b-0", className, )} {...props} /> );}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) { return ( <tr data-slot="table-row" className={cn( "border-b border-border transition-colors hover:bg-[color-mix(in_srgb,var(--muted)_50%,var(--card))] has-aria-expanded:bg-[color-mix(in_srgb,var(--muted)_50%,var(--card))] data-[state=selected]:bg-[color-mix(in_srgb,var(--brand)_7%,var(--card))]", className, )} {...props} /> );}
type NumericProps = { /** ✦ Right-aligned, mono, tabular numbers (DESIGN §4 rule 10). */ numeric?: boolean;};
function TableHead({ className, numeric, ...props}: React.ComponentProps<"th"> & NumericProps) { return ( <th data-slot="table-head" data-numeric={numeric ? "" : undefined} className={cn( "px-3 py-2.5 text-left align-middle text-[12.5px] font-medium whitespace-nowrap text-muted-foreground data-[numeric]:text-right [&:has([role=checkbox])]:pr-0", className, )} {...props} /> );}
function TableCell({ className, numeric, trend, ...props}: React.ComponentProps<"td"> & NumericProps & { /** ✦ Colours a numeric value: up = brand text, down = destructive text. */ trend?: "up" | "down"; }) { return ( <td data-slot="table-cell" data-numeric={numeric ? "" : undefined} data-trend={trend} className={cn( "p-3 align-middle whitespace-nowrap data-[numeric]:text-right data-[numeric]:font-mono data-[numeric]:tabular-nums data-[trend=down]:text-destructive-text data-[trend=up]:text-success-text [&:has([role=checkbox])]:pr-0", className, )} {...props} /> );}
function TableCaption({ className, ...props}: React.ComponentProps<"caption">) { return ( <caption data-slot="table-caption" className={cn( "border-t border-border p-2.5 text-center text-[13px] text-muted-foreground", className, )} {...props} /> );}
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow,};<script setup lang="ts">import type { HTMLAttributes } from "vue";import { cn } from "@/registry/edmi/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"];}>();</script>
<template> <div data-slot="table-container" class="relative w-full overflow-x-auto"> <table data-slot="table" :class="cn('w-full caption-bottom border-collapse text-[13.5px]', props.class)" > <slot /> </table> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue";import { cn } from "@/registry/edmi/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"];}>();</script>
<template> <tbody data-slot="table-body" :class="cn('[&_tr:last-child]:border-0', props.class)" > <slot /> </tbody></template><script setup lang="ts">import type { HTMLAttributes } from "vue";import { cn } from "@/registry/edmi/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"];}>();</script>
<template> <caption data-slot="table-caption" :class="cn('border-t border-border p-2.5 text-center text-[13px] text-muted-foreground', props.class)" > <slot /> </caption></template><script setup lang="ts">import type { HTMLAttributes } from "vue";import { cn } from "@/registry/edmi/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"]; /** ✦ Right-aligned, mono, tabular numbers (DESIGN §4 rule 10). */ numeric?: boolean; /** ✦ Colours a numeric value: up = brand text, down = destructive text. */ trend?: "up" | "down";}>();</script>
<template> <td data-slot="table-cell" :data-numeric="numeric ? '' : undefined" :data-trend="trend" :class="cn('p-3 align-middle whitespace-nowrap data-[numeric]:text-right data-[numeric]:font-mono data-[numeric]:tabular-nums data-[trend=down]:text-destructive-text data-[trend=up]:text-success-text [&:has([role=checkbox])]:pr-0', props.class)" > <slot /> </td></template><script setup lang="ts">import type { HTMLAttributes } from "vue";import { cn } from "@/registry/edmi/lib/utils";
import TableCell from "./TableCell.vue";import TableRow from "./TableRow.vue";
const props = withDefaults( defineProps<{ class?: HTMLAttributes["class"]; colspan?: number; }>(), { colspan: 1 },);</script>
<template> <TableRow class="hover:bg-transparent"> <TableCell :colspan="props.colspan" :class="cn('text-foreground', props.class)"> <div class="flex h-24 items-center justify-center text-muted-foreground"> <slot /> </div> </TableCell> </TableRow></template><script setup lang="ts">import type { HTMLAttributes } from "vue";import { cn } from "@/registry/edmi/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"];}>();</script>
<template> <tfoot data-slot="table-footer" :class="cn('border-t bg-muted font-semibold [&>tr]:last:border-b-0', props.class)" > <slot /> </tfoot></template><script setup lang="ts">import type { HTMLAttributes } from "vue";import { cn } from "@/registry/edmi/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"]; /** ✦ Right-aligned, mono, tabular numbers (DESIGN §4 rule 10). */ numeric?: boolean;}>();</script>
<template> <th data-slot="table-head" :data-numeric="numeric ? '' : undefined" :class="cn('px-3 py-2.5 text-left align-middle text-[12.5px] font-medium whitespace-nowrap text-muted-foreground data-[numeric]:text-right [&:has([role=checkbox])]:pr-0', props.class)" > <slot /> </th></template><script setup lang="ts">import type { HTMLAttributes } from "vue";import { cn } from "@/registry/edmi/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"];}>();</script>
<template> <thead data-slot="table-header" :class="cn('[&_tr]:border-b', props.class)" > <slot /> </thead></template><script setup lang="ts">import type { HTMLAttributes } from "vue";import { cn } from "@/registry/edmi/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"];}>();</script>
<template> <tr data-slot="table-row" :class="cn('border-b border-border transition-colors hover:bg-[color-mix(in_srgb,var(--muted)_50%,var(--card))] has-aria-expanded:bg-[color-mix(in_srgb,var(--muted)_50%,var(--card))] data-[state=selected]:bg-[color-mix(in_srgb,var(--brand)_7%,var(--card))]', props.class)" > <slot /> </tr></template>export { default as Table } from "./Table.vue";export { default as TableBody } from "./TableBody.vue";export { default as TableCaption } from "./TableCaption.vue";export { default as TableCell } from "./TableCell.vue";export { default as TableEmpty } from "./TableEmpty.vue";export { default as TableFooter } from "./TableFooter.vue";export { default as TableHead } from "./TableHead.vue";export { default as TableHeader } from "./TableHeader.vue";export { default as TableRow } from "./TableRow.vue";import Root from "./table.svelte";import Body from "./table-body.svelte";import Caption from "./table-caption.svelte";import Cell from "./table-cell.svelte";import Empty from "./table-empty.svelte";import Footer from "./table-footer.svelte";import Head from "./table-head.svelte";import Header from "./table-header.svelte";import Row from "./table-row.svelte";
export { Body, Body as TableBody, Caption, Caption as TableCaption, Cell, Cell as TableCell, Empty, Empty as TableEmpty, Footer, Footer as TableFooter, Head, Head as TableHead, Header, Header as TableHeader, Root, // Root as Table, Row, Row as TableRow,};<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();</script>
<tbody bind:this={ref} data-slot="table-body" class={cn("[&_tr:last-child]:border-0", className)} {...restProps}> {@render children?.()}</tbody><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();</script>
<caption bind:this={ref} data-slot="table-caption" class={cn("border-t border-border p-2.5 text-center text-[13px] text-muted-foreground", className)} {...restProps}> {@render children?.()}</caption><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLTdAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, numeric = false, trend, ...restProps }: WithElementRef<HTMLTdAttributes> & { /** ✦ Right-aligned, mono, tabular numbers (DESIGN §4 rule 10). */ numeric?: boolean; /** ✦ Colours a numeric value: up = brand text, down = destructive text. */ trend?: "up" | "down"; } = $props();</script>
<td bind:this={ref} data-slot="table-cell" data-numeric={numeric ? "" : undefined} data-trend={trend} class={cn( "p-3 align-middle whitespace-nowrap data-[numeric]:text-right data-[numeric]:font-mono data-[numeric]:tabular-nums data-[trend=down]:text-destructive-text data-[trend=up]:text-success-text [&:has([role=checkbox])]:pr-0", className )} {...restProps}> {@render children?.()}</td><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLTdAttributes } from "svelte/elements"; import TableRow from "./table-row.svelte"; import TableCell from "./table-cell.svelte";
let { ref = $bindable(null), class: className, children, colspan = 1, ...restProps }: WithElementRef<HTMLTdAttributes> = $props();</script>
<TableRow class="hover:bg-transparent"> <TableCell bind:ref {colspan} class={cn("text-foreground", className)} {...restProps}> <div class="flex h-24 items-center justify-center text-muted-foreground"> {@render children?.()} </div> </TableCell></TableRow><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();</script>
<tfoot bind:this={ref} data-slot="table-footer" class={cn("border-t bg-muted font-semibold [&>tr]:last:border-b-0", className)} {...restProps}> {@render children?.()}</tfoot><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLThAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, numeric = false, ...restProps }: WithElementRef<HTMLThAttributes> & { /** ✦ Right-aligned, mono, tabular numbers (DESIGN §4 rule 10). */ numeric?: boolean; } = $props();</script>
<th bind:this={ref} data-slot="table-head" data-numeric={numeric ? "" : undefined} class={cn( "px-3 py-2.5 text-left align-middle text-[12.5px] font-medium whitespace-nowrap text-muted-foreground data-[numeric]:text-right [&:has([role=checkbox])]:pr-0", className )} {...restProps}> {@render children?.()}</th><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();</script>
<thead bind:this={ref} data-slot="table-header" class={cn("[&_tr]:border-b", className)} {...restProps}> {@render children?.()}</thead><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();</script>
<tr bind:this={ref} data-slot="table-row" class={cn("border-b border-border transition-colors hover:bg-[color-mix(in_srgb,var(--muted)_50%,var(--card))] has-aria-expanded:bg-[color-mix(in_srgb,var(--muted)_50%,var(--card))] data-[state=selected]:bg-[color-mix(in_srgb,var(--brand)_7%,var(--card))]", className)} {...restProps}> {@render children?.()}</tr><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLTableAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLTableAttributes> = $props();</script>
<div data-slot="table-container" class="relative w-full overflow-x-auto"> <table bind:this={ref} data-slot="table" class={cn("w-full caption-bottom border-collapse text-[13.5px]", className)} {...restProps} > {@render children?.()} </table></div>| Prop | Type | Default |
|---|---|---|
TableHead numeric ✦ |
boolean |
false |
TableCell numeric ✦ |
boolean |
false |
TableCell trend ✦ |
up | down |
none |
Parts: Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption. All stock parts and data-slots are kept; TableRow highlights data-[state=selected] with a soft brand tint.
✦ Edmi additions
Section titled “✦ Edmi additions”numericright-aligns and sets mono tabular numbers (DESIGN §4 rule 10).trend="up|down"colours a value with--success-text/--destructive-text.- Footer sits on
--muted; the caption renders as a bordered footer line.
Vue / Svelte differences
Section titled “Vue / Svelte differences”- Vue and Svelte include an extra
TableEmptypart for an empty-state row.