Skip to content
edmi-ui

Table

A plain, styled HTML table. Numbers are right-aligned and use the mono face; the footer holds totals.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
TokenPriceWeight24h
NVDAx
NVIDIA
$188.2032.0%+2.41%
MSFTx
Microsoft
$512.1028.0%+0.88%
AAPLx
Apple
$231.4524.0%-0.62%
ANTHRP-pre
Anthropic pre-IPO
$61.3016.0%+4.10%
Total · 4 tokensNAV $0.9998100%+0.53%
Weights as of the last rebalance.
TokenPriceWeight24h
NVDAx
NVIDIA
$188.2032.0%+2.41%
MSFTx
Microsoft
$512.1028.0%+0.88%
AAPLx
Apple
$231.4524.0%-0.62%
ANTHRP-pre
Anthropic pre-IPO
$61.3016.0%+4.10%
Total · 4 tokensNAV $0.9998100%+0.53%
Weights as of the last rebalance.
Token Price Weight 24h
NVDAx
NVIDIA
$188.20 32.0% +2.41%
MSFTx
Microsoft
$512.10 28.0% +0.88%
AAPLx
Apple
$231.45 24.0% -0.62%
ANTHRP-pre
Anthropic pre-IPO
$61.30 16.0% +4.10%
Total · 4 tokens NAV $0.9998 100% +0.53%
Weights as of the last rebalance.
Terminal window
npx shadcn@latest add @edmi-ui/table
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/table
Terminal window
yarn dlx shadcn@latest add @edmi-ui/table
Terminal window
bunx shadcn@latest add @edmi-ui/table

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/table
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/table
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/table
Terminal window
bunx shadcn-vue@latest add @edmi-ui/table

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/table.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/table.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/table.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/table.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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>
registry/ui/table.tsx
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,
};
registry/ui/table/Table.vue
<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>
registry/ui/table/TableBody.vue
<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>
registry/ui/table/TableCaption.vue
<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>
registry/ui/table/TableCell.vue
<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>
registry/ui/table/TableEmpty.vue
<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>
registry/ui/table/TableFooter.vue
<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>
registry/ui/table/TableHead.vue
<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>
registry/ui/table/TableHeader.vue
<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>
registry/ui/table/TableRow.vue
<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>
registry/ui/table/index.ts
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";
src/lib/registry/ui/table/index.ts
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,
};
src/lib/registry/ui/table/table-body.svelte
<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>
src/lib/registry/ui/table/table-caption.svelte
<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>
src/lib/registry/ui/table/table-cell.svelte
<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>
src/lib/registry/ui/table/table-empty.svelte
<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>
src/lib/registry/ui/table/table-footer.svelte
<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>
src/lib/registry/ui/table/table-head.svelte
<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>
src/lib/registry/ui/table/table-header.svelte
<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>
src/lib/registry/ui/table/table-row.svelte
<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>
src/lib/registry/ui/table/table.svelte
<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.

  • numeric right-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 and Svelte include an extra TableEmpty part for an empty-state row.