# Datatable 17 - Column Actions Table

> A customer table with a dropdown on every column title to sort, pin, and move columns, plus a Columns menu to show or hide them. Useful for admin tables users can customize.

A shadcn/ui block from the [@shadcn-dashboard](https://shadcndashboard.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcn-dashboard/datatable-17).

- Page, with a live preview: https://shadcn.exchange/r/shadcn-dashboard/datatable-17
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcn-dashboard/datatable-17-light-73f41db2e48cc768.png
- Registry item: https://shadcndashboard.dev/r/datatable-17.json
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/shadcn-dashboard/datatable-17.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `avatar`, `badge`, `button`, `card`, `dropdown-menu`, `table`

## Files

### `src/components/shadcn-dashboard/blocks/datatable/datatable-17/page.tsx`

```tsx
import ColumnPinningTable from "@/components/shadcn-dashboard/blocks/datatable/datatable-17/datatable"

const page = () => {
  return <ColumnPinningTable />
}

export default page
```

### `src/components/shadcn-dashboard/blocks/datatable/datatable-17/datatable.tsx`

```tsx
"use client";

import { useMemo, useState } from "react";
import {
  ArrowDownIcon,
  ArrowLeftToLineIcon,
  ArrowRightToLineIcon,
  ArrowUpIcon,
  ChevronLeftIcon,
  ChevronRightIcon,
  ChevronDownIcon,
  Columns3Icon,
  CrownIcon,
  PinOffIcon,
  RotateCcwIcon,
  SproutIcon,
  XIcon,
  ZapIcon,
  type LucideIcon,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardAction,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";

type Customer = {
  id: number;
  name: string;
  email: string;
  avatar: string;
  company: string;
  role: string;
  plan: "Free" | "Pro" | "Enterprise";
  status: "Active" | "Trial" | "Churned";
  mrr: number;
};

type Pin = "left" | "right";
type SortState = { id: string; dir: "asc" | "desc" } | null;

type Column = {
  id: string;
  label: string;
  align?: "right";
  sortValue: (row: Customer) => string | number;
  render: (row: Customer) => React.ReactNode;
};

const AVATAR =
  "https://images.shadcnspace.com/assets/shadcn-dashboard/profile/user-";

const customers: Customer[] = [
  { id: 1, name: "Sunil Joshi", email: "sunil@acme.com", avatar: `${AVATAR}1.webp`, company: "Acme Corp", role: "CTO", plan: "Enterprise", status: "Active", mrr: 2400 },
  { id: 2, name: "Andrew McDownland", email: "andrew@globex.com", avatar: `${AVATAR}8.webp`, company: "Globex", role: "Head of Product", plan: "Pro", status: "Active", mrr: 480 },
  { id: 3, name: "Christopher Jamil", email: "chris@initech.com", avatar: `${AVATAR}2.webp`, company: "Initech", role: "Engineer", plan: "Pro", status: "Trial", mrr: 0 },
  { id: 4, name: "Nirav Joshi", email: "nirav@umbrella.com", avatar: `${AVATAR}3.webp`, company: "Umbrella", role: "Founder", plan: "Enterprise", status: "Active", mrr: 3200 },
  { id: 5, name: "Priya Shah", email: "priya@hooli.com", avatar: `${AVATAR}4.webp`, company: "Hooli", role: "Designer", plan: "Free", status: "Churned", mrr: 0 },
  { id: 6, name: "Daniel Kim", email: "daniel@stark.com", avatar: `${AVATAR}5.webp`, company: "Stark Industries", role: "VP Engineering", plan: "Enterprise", status: "Active", mrr: 4100 },
  { id: 7, name: "Maria Lopez", email: "maria@wayne.com", avatar: `${AVATAR}6.webp`, company: "Wayne Enterprises", role: "Marketing Lead", plan: "Pro", status: "Active", mrr: 520 },
  { id: 8, name: "Omar Haddad", email: "omar@wonka.com", avatar: `${AVATAR}7.webp`, company: "Wonka Labs", role: "Analyst", plan: "Free", status: "Trial", mrr: 0 },
];

const STATUS_STYLES: Record<Customer["status"], string> = {
  Active: "bg-chart-2/10! text-chart-2!",
  Trial: "bg-orange-400/10! text-orange-400!",
  Churned: "bg-destructive/10! text-destructive!",
};

const PLAN_STYLES: Record<Customer["plan"], { icon: LucideIcon; className: string }> = {
  Enterprise: { icon: CrownIcon, className: "text-blue-500" },
  Pro: { icon: ZapIcon, className: "text-sky-400" },
  Free: { icon: SproutIcon, className: "text-teal-400" },
};

const PLAN_RANK: Record<Customer["plan"], number> = {
  Free: 0,
  Pro: 1,
  Enterprise: 2,
};

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
});

const columns: Column[] = [
  {
    id: "name",
    label: "Customer",
    sortValue: (r) => r.name,
    render: (r) => (
      <div className="flex items-center gap-3">
        <Avatar>
          <AvatarImage src={r.avatar} alt={r.name} />
          <AvatarFallback>{r.name[0]}</AvatarFallback>
        </Avatar>
        <div className="flex flex-col">
          <h6 className="text-sm font-medium">{r.name}</h6>
          <span className="text-xs text-muted-foreground">{r.email}</span>
        </div>
      </div>
    ),
  },
  {
    id: "company",
    label: "Company",
    sortValue: (r) => r.company,
    render: (r) => r.company,
  },
  {
    id: "status",
    label: "Status",
    sortValue: (r) => r.status,
    render: (r) => <Badge className={STATUS_STYLES[r.status]}>{r.status}</Badge>,
  },
  {
    id: "role",
    label: "Role",
    sortValue: (r) => r.role,
    render: (r) => r.role,
  },
  {
    id: "plan",
    label: "Plan",
    sortValue: (r) => PLAN_RANK[r.plan],
    render: (r) => {
      const plan = PLAN_STYLES[r.plan];
      const PlanIcon = plan.icon;
      return (
        <span className={cn("inline-flex items-center gap-1.5", plan.className)}>
          <PlanIcon className="size-4" />
          {r.plan}
        </span>
      );
    },
  },
  {
    id: "mrr",
    label: "MRR",
    align: "right",
    sortValue: (r) => r.mrr,
    render: (r) => (r.mrr === 0 ? "\u2014" : currency.format(r.mrr)),
  },
];

const DEFAULT_ORDER = columns.map((c) => c.id);

function ColumnMenuTable() {
  const [order, setOrder] = useState<string[]>(DEFAULT_ORDER);
  const [hidden, setHidden] = useState<string[]>([]);
  const [pins, setPins] = useState<Record<string, Pin | undefined>>({});
  const [sort, setSort] = useState<SortState>(null);

  const byId = useMemo(
    () => new Map(columns.map((c) => [c.id, c] as const)),
    [],
  );

  const visible = order.filter((id) => !hidden.includes(id));
  const ordered = [
    ...visible.filter((id) => pins[id] === "left"),
    ...visible.filter((id) => !pins[id]),
    ...visible.filter((id) => pins[id] === "right"),
  ].map((id) => byId.get(id)!);

  const rows = useMemo(() => {
    if (!sort) return customers;
    const col = byId.get(sort.id);
    if (!col) return customers;
    const dir = sort.dir === "asc" ? 1 : -1;
    return [...customers].sort((a, b) => {
      const av = col.sortValue(a);
      const bv = col.sortValue(b);
      if (typeof av === "number" && typeof bv === "number") return (av - bv) * dir;
      return String(av).localeCompare(String(bv)) * dir;
    });
  }, [sort, byId]);

  const setPin = (id: string, pin?: Pin) =>
    setPins((prev) => ({ ...prev, [id]: pin }));

  const move = (id: string, delta: -1 | 1) =>
    setOrder((prev) => {
      const group = ordered.map((c) => c.id);
      const from = group.indexOf(id);
      const target = group[from + delta];
      if (target === undefined) return prev;
      const next = [...prev];
      const i = next.indexOf(id);
      const j = next.indexOf(target);
      [next[i], next[j]] = [next[j], next[i]];
      return next;
    });

  const toggleColumn = (id: string, show: boolean) =>
    setHidden((prev) => (show ? prev.filter((h) => h !== id) : [...prev, id]));

  const isChanged =
    sort !== null ||
    hidden.length > 0 ||
    Object.values(pins).some(Boolean) ||
    order.some((id, i) => id !== DEFAULT_ORDER[i]);

  const reset = () => {
    setOrder(DEFAULT_ORDER);
    setHidden([]);
    setPins({});
    setSort(null);
  };

  const pinCell = (c: Column) => {
    const pin = pins[c.id];
    const lastLeft =
      pin === "left" && ordered.filter((o) => pins[o.id] === "left").at(-1)?.id === c.id;
    const firstRight =
      pin === "right" && ordered.find((o) => pins[o.id] === "right")?.id === c.id;
    return cn(
      pin && "bg-muted/30",
      lastLeft && "border-r border-border",
      firstRight && "border-l border-border",
    );
  };

  return (
    <section className="py-8 px-4 max-w-7xl w-full mx-auto">
      <Card className="w-full py-0 gap-0 ring-0 border border-border">
        <CardHeader className="flex items-center justify-between flex-wrap gap-4 p-3.5">
          <div>
            <CardTitle>Customers</CardTitle>
            <CardDescription>
              Click a column title to sort, pin, move or hide it
            </CardDescription>
          </div>
          <CardAction className="flex items-center gap-2">
            <DropdownMenu>
              <DropdownMenuTrigger
                render={
                  <Button variant="outline" className="cursor-pointer">
                    <Columns3Icon className="size-4" />
                    Columns
                  </Button>
                }
              />
              <DropdownMenuContent align="end" className="w-44">
                {columns.map((col) => {
                  const shown = !hidden.includes(col.id);
                  return (
                    <DropdownMenuCheckboxItem
                      key={col.id}
                      className="cursor-pointer"
                      checked={shown}
                      disabled={shown && visible.length === 1}
                      onCheckedChange={(checked) => toggleColumn(col.id, !!checked)}
                    >
                      {col.label}
                    </DropdownMenuCheckboxItem>
                  );
                })}
              </DropdownMenuContent>
            </DropdownMenu>
            <Button
              variant="outline"
              className="cursor-pointer"
              disabled={!isChanged}
              onClick={reset}
            >
              <RotateCcwIcon className="size-4" />
              Reset
            </Button>
          </CardAction>
        </CardHeader>

        <div className="border-t border-border">
          <Table className="min-w-full">
            <TableHeader>
              <TableRow>
                {ordered.map((c, index) => {
                  const pin = pins[c.id];
                  const sorted = sort?.id === c.id ? sort.dir : null;
                  return (
                    <TableHead
                      key={c.id}
                      className={cn(
                        "text-base font-medium py-3 text-left border-b border-border px-4 h-auto",
                        c.align === "right" && "text-right",
                        pinCell(c),
                      )}
                    >
                      <DropdownMenu>
                        <DropdownMenuTrigger
                          render={
                            <button
                              type="button"
                              className={cn(
                                "group/title inline-flex cursor-pointer items-center gap-1.5 text-base font-medium outline-none focus-visible:underline",
                                c.align === "right" && "ml-auto flex",
                              )}
                            >
                              {c.label}
                              {sorted === "asc" && <ArrowUpIcon className="size-3.5" />}
                              {sorted === "desc" && <ArrowDownIcon className="size-3.5" />}
                              {pin && (
                                <ArrowLeftToLineIcon
                                  className={cn(
                                    "size-3.5 text-muted-foreground",
                                    pin === "right" && "rotate-180",
                                  )}
                                />
                              )}
                              <ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-aria-expanded/title:rotate-180" />
                            </button>
                          }
                        />
                        <DropdownMenuContent
                          align={c.align === "right" ? "end" : "start"}
                          className="w-48"
                        >
                          <DropdownMenuItem
                            className="cursor-pointer"
                            onClick={() => setSort({ id: c.id, dir: "asc" })}
                          >
                            <ArrowUpIcon /> Asc
                          </DropdownMenuItem>
                          <DropdownMenuItem
                            className="cursor-pointer"
                            onClick={() => setSort({ id: c.id, dir: "desc" })}
                          >
                            <ArrowDownIcon /> Desc
                          </DropdownMenuItem>
                          {sorted && (
                            <DropdownMenuItem
                              className="cursor-pointer"
                              onClick={() => setSort(null)}
                            >
                              <XIcon /> Clear sort
                            </DropdownMenuItem>
                          )}

                          <DropdownMenuSeparator />
                          <DropdownMenuItem
                            className="cursor-pointer"
                            onClick={() => setPin(c.id, "left")}
                          >
                            <ArrowLeftToLineIcon /> Pin to left
                          </DropdownMenuItem>
                          <DropdownMenuItem
                            className="cursor-pointer"
                            onClick={() => setPin(c.id, "right")}
                          >
                            <ArrowRightToLineIcon /> Pin to right
                          </DropdownMenuItem>
                          {pin && (
                            <DropdownMenuItem
                              className="cursor-pointer"
                              onClick={() => setPin(c.id, undefined)}
                            >
                              <PinOffIcon /> Unpin
                            </DropdownMenuItem>
                          )}

                          <DropdownMenuSeparator />
                          <DropdownMenuItem
                            className="cursor-pointer"
                            disabled={index === 0}
                            onClick={() => move(c.id, -1)}
                          >
                            <ChevronLeftIcon /> Move to left
                          </DropdownMenuItem>
                          <DropdownMenuItem
                            className="cursor-pointer"
                            disabled={index === ordered.length - 1}
                            onClick={() => move(c.id, 1)}
                          >
                            <ChevronRightIcon /> Move to right
                          </DropdownMenuItem>

                        </DropdownMenuContent>
                      </DropdownMenu>
                    </TableHead>
                  );
                })}
              </TableRow>
            </TableHeader>
            <TableBody className="divide-y divide-border">
              {rows.map((row) => (
                <TableRow key={row.id} className="hover:bg-muted/40">
                  {ordered.map((c) => (
                    <TableCell
                      key={c.id}
                      className={cn(
                        "py-3 px-4",
                        c.align === "right" && "text-right",
                        pinCell(c),
                      )}
                    >
                      {c.render(row)}
                    </TableCell>
                  ))}
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </div>
      </Card>
    </section>
  );
}

export default ColumnMenuTable;
```
