Vela UI Docsvenizia.ai

Table

A semantic table with aligned cells and helpers for media, captions, trends and row actions.

npx shadcn@latest add @velaui/table
npx shadcn@latest add @velaui/table

Examples

  • Default
  • Bordered

View code

Table · Default

Installation

pnpm dlx velaui add @velaui/table
pnpm dlx velaui add @velaui/table

Code

table-default.tsx
import {
  Table,
  TableBody,
  TableCell,
  TableCellCaption,
  TableCellText,
  TableCellTrend,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

const invoices = [
  { id: "INV-1042", customer: "Linden Supply Co.", amount: "$1,250.00", change: "12%", trend: "up" },
  { id: "INV-1043", customer: "Harbor & Pine", amount: "$860.00", change: "4%", trend: "down" },
  { id: "INV-1044", customer: "Tanaka Freight", amount: "$2,100.00", change: "8%", trend: "up" },
] as const;

export default function Example() {
  return (
    <Table className="max-w-xl">
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead>Customer</TableHead>
          <TableHead align="right">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.id}>
            <TableCell>
              <TableCellText bold>{invoice.id}</TableCellText>
              <TableCellCaption>Due in 14 days</TableCellCaption>
            </TableCell>
            <TableCell>{invoice.customer}</TableCell>
            <TableCell align="right">
              <TableCellText>{invoice.amount}</TableCellText>
              <TableCellTrend trend={invoice.trend}>{invoice.change}</TableCellTrend>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

View code

Table · Bordered

Installation

pnpm dlx velaui add @velaui/table
pnpm dlx velaui add @velaui/table

Code

table-bordered.tsx
import { Button } from "@/components/ui/button";
import {
  Table,
  TableBody,
  TableCell,
  TableCellActions,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

const members = [
  { name: "Ava Turner", role: "Owner" },
  { name: "Noah Reyes", role: "Editor" },
  { name: "Iris Park", role: "Viewer" },
];

export default function Example() {
  return (
    <Table bordered>
      <TableHeader>
        <TableRow>
          <TableHead>Name</TableHead>
          <TableHead>Role</TableHead>
          <TableHead align="right">Actions</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {members.map((member) => (
          <TableRow key={member.name}>
            <TableCell>{member.name}</TableCell>
            <TableCell>{member.role}</TableCell>
            <TableCell align="right">
              <TableCellActions className="justify-end">
                <Button variant="ghost" size="sm">
                  Edit
                </Button>
              </TableCellActions>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

Usage

import {
  Table,
  TableBody,
  TableCell,
  TableCellCaption,
  TableCellText,
  TableCellTrend,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

const invoices = [
  { id: "INV-1042", customer: "Linden Supply Co.", amount: "$1,250.00", change: "12%", trend: "up" },
  { id: "INV-1043", customer: "Harbor & Pine", amount: "$860.00", change: "4%", trend: "down" },
  { id: "INV-1044", customer: "Tanaka Freight", amount: "$2,100.00", change: "8%", trend: "up" },
] as const;

export default function Example() {
  return (
    <Table className="max-w-xl">
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead>Customer</TableHead>
          <TableHead align="right">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.id}>
            <TableCell>
              <TableCellText bold>{invoice.id}</TableCellText>
              <TableCellCaption>Due in 14 days</TableCellCaption>
            </TableCell>
            <TableCell>{invoice.customer}</TableCell>
            <TableCell align="right">
              <TableCellText>{invoice.amount}</TableCellText>
              <TableCellTrend trend={invoice.trend}>{invoice.change}</TableCellTrend>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

Props

Table

PropTypeDefault
borderedbooleanfalse

Takes these and every prop of TableProps.

TableHeader

Takes every prop of React.ComponentProps<"thead">.

TableBody

Takes every prop of React.ComponentProps<"tbody">.

TableFooter

Takes every prop of React.ComponentProps<"tfoot">.

TableRow

Takes every prop of React.ComponentProps<"tr">.

TableHead

PropTypeDefault
align"left" | "right""left"

Takes these and every prop of TableHeadProps.

TableCell

PropTypeDefault
align"left" | "right""left"

Takes these and every prop of TableCellProps.

TableCaption

Takes every prop of React.ComponentProps<"caption">.

TableCellMedia

Takes every prop of React.ComponentProps<"div">.

TableCellText

PropTypeDefault
boldboolean—

Takes these and every prop of TableCellTextProps.

TableCellCaption

Takes every prop of React.ComponentProps<"span">.

TableCellTrend

PropTypeDefault
trend"up" | "down""up"

Takes these and every prop of TableCellTrendProps.

TableCellActions

Takes every prop of React.ComponentProps<"div">.

Dependencies

npm
class-variance-authority@0.7.1
Installs with it
2 icons

Source

components/ui/table.tsx
/* eslint-disable react-refresh/only-export-components */
"use client";

import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { ChevronDown } from "@/components/icons/chevron-down";
import { ChevronUp } from "@/components/icons/chevron-up";

import { cn } from "@/lib/utils";

/* -------------------------------------------------------------------------- */
/*                                    Shell                                    */
/* -------------------------------------------------------------------------- */

export type TableProps = React.ComponentProps<"table"> & {
  /**
   * Frame the table in a rounded outline. The design runs the table flush with
   * its column edges, so the frame also restores the outer padding the first
   * and last columns drop, and folds the last row's rule into the outline.
   */
  bordered?: boolean;
};

function Table({ className, bordered = false, ...props }: TableProps) {
  return (
    <div
      data-slot="table-container"
      data-bordered={bordered || undefined}
      className={cn(
        "relative w-full overflow-x-auto",
        bordered &&
          "rounded-lg border border-border-default [&_tbody_tr:last-child>td]:border-b-0 [&_td:first-child]:pl-3 [&_td:last-child]:pr-3 [&_th:first-child]:pl-3 [&_th:last-child]:pr-3",
      )}
    >
      <table
        data-slot="table"
        className={cn(
          "w-full caption-bottom border-collapse text-base leading-6 text-text-strong",
          className,
        )}
        {...props}
      />
    </div>
  );
}

function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
  return <thead data-slot="table-header" className={cn(className)} {...props} />;
}

function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
  return <tbody data-slot="table-body" className={cn(className)} {...props} />;
}

function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
  return (
    <tfoot
      data-slot="table-footer"
      className={cn("[&>tr:last-child]:border-b-0", className)}
      {...props}
    />
  );
}

function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
  return (
    <tr
      data-slot="table-row"
      className={cn("group/table-row", className)}
      {...props}
    />
  );
}

function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
  return (
    <caption
      data-slot="table-caption"
      className={cn("mt-4 text-sm text-text-note", className)}
      {...props}
    />
  );
}

/* -------------------------------------------------------------------------- */
/*                                    Cells                                    */
/* -------------------------------------------------------------------------- */

// Column position is positional, so it stays in CSS: the outer edges of the
// row lose their outside padding, every inner edge keeps px-3.
const tableHeadVariants = cva(
  "h-10 border-b border-border-default px-3 py-3 align-middle text-xs leading-[1.3] font-medium whitespace-nowrap text-text-muted transition-colors first:pl-0 last:pr-0 hover:text-text-strong",
  {
    variants: {
      align: {
        left: "text-left",
        right: "text-right",
      },
    },
    defaultVariants: {
      align: "left",
    },
  },
);

const tableCellVariants = cva(
  // Focus draws a 2px stroke in the design — rendered as an inset ring so
  // entering the state cannot resize the cell (and shove the column).
  "h-12 border-b border-border-default p-3 align-middle text-base leading-6 tracking-[-0.24px] text-text-strong outline-none transition-colors first:pl-0 last:pr-0 hover:bg-bg-surface-1 focus-visible:bg-bg-surface-1 focus-visible:ring-2 focus-visible:ring-bg-surface-2 focus-visible:ring-inset data-[state=selected]:bg-bg-surface-1 group-data-[state=selected]/table-row:bg-bg-surface-1",
  {
    variants: {
      align: {
        left: "text-left",
        right: "text-right",
      },
    },
    defaultVariants: {
      align: "left",
    },
  },
);

export type TableHeadProps = React.ComponentProps<"th"> &
  VariantProps<typeof tableHeadVariants>;

function TableHead({ className, align = "left", ...props }: TableHeadProps) {
  return (
    <th
      data-slot="table-head"
      data-align={align}
      className={cn(tableHeadVariants({ align }), className)}
      {...props}
    />
  );
}

export type TableCellProps = React.ComponentProps<"td"> &
  VariantProps<typeof tableCellVariants>;

function TableCell({ className, align = "left", ...props }: TableCellProps) {
  return (
    <td
      data-slot="table-cell"
      data-align={align}
      className={cn(tableCellVariants({ align }), className)}
      {...props}
    />
  );
}

/* -------------------------------------------------------------------------- */
/*                                Cell contents                                */
/* -------------------------------------------------------------------------- */

/**
 * Leading media (avatar, photo, icon, circle icon) beside the cell's text
 * stack — the design gives every one of those types the same 12px gap.
 */
function TableCellMedia({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="table-cell-media"
      className={cn(
        "flex items-center gap-3 [&_svg]:size-6 [&_svg]:shrink-0 [&_svg]:text-icon-strong",
        className,
      )}
      {...props}
    />
  );
}

const tableCellTextVariants = cva(
  "block text-base leading-6 tracking-[-0.24px] text-text-strong",
  {
    variants: {
      bold: {
        true: "font-medium",
        false: "font-normal",
      },
    },
    defaultVariants: {
      bold: false,
    },
  },
);

export type TableCellTextProps = React.ComponentProps<"span"> &
  VariantProps<typeof tableCellTextVariants>;

function TableCellText({ className, bold, ...props }: TableCellTextProps) {
  return (
    <span
      data-slot="table-cell-text"
      className={cn(tableCellTextVariants({ bold }), className)}
      {...props}
    />
  );
}

function TableCellCaption({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="table-cell-caption"
      className={cn("block text-xs leading-4 text-text-note", className)}
      {...props}
    />
  );
}

const tableCellTrendVariants = cva(
  "inline-flex items-center gap-1 text-xs leading-[1.3] font-medium [&_svg]:size-4 [&_svg]:shrink-0",
  {
    variants: {
      trend: {
        up: "text-success-middle",
        down: "text-negative-middle",
      },
    },
    defaultVariants: {
      trend: "up",
    },
  },
);

export type TableCellTrendProps = React.ComponentProps<"span"> &
  VariantProps<typeof tableCellTrendVariants>;

function TableCellTrend({
  className,
  children,
  trend = "up",
  ...props
}: TableCellTrendProps) {
  const TrendIcon = trend === "down" ? ChevronDown : ChevronUp;

  return (
    <span
      data-slot="table-cell-trend"
      data-trend={trend}
      className={cn(tableCellTrendVariants({ trend }), className)}
      {...props}
    >
      {children}
      <TrendIcon />
    </span>
  );
}

/** Trailing controls of a row — bare buttons, icon buttons, or both. */
function TableCellActions({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="table-cell-actions"
      className={cn("flex items-center gap-2", className)}
      {...props}
    />
  );
}

export {
  Table,
  TableHeader,
  TableBody,
  TableFooter,
  TableRow,
  TableHead,
  TableCell,
  TableCaption,
  TableCellMedia,
  TableCellText,
  TableCellCaption,
  TableCellTrend,
  TableCellActions,
  tableHeadVariants,
  tableCellVariants,
  tableCellTextVariants,
  tableCellTrendVariants,
};