# Datatable 16 - Master Detail Table

> A team directory table with live search and department tabs. Clicking a row opens a side sheet with the full profile. Useful for HR, CRM, and team dashboards.

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-16).

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

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/shadcn-dashboard/datatable-16.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`, `input-group`, `sheet`, `table`, `tabs`

## Files

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

```tsx
import MasterDetailTable from "@/components/shadcn-dashboard/blocks/datatable/datatable-16/datatable"

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

export default page
```

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

```tsx
"use client";

import { useEffect, useMemo, useState } from "react";
import {
  CalendarIcon,
  ChevronRightIcon,
  MailIcon,
  MapPinIcon,
  CodeIcon,
  MegaphoneIcon,
  PaletteIcon,
  SearchIcon,
  ActivityIcon,
  AtomIcon,
  CloudIcon,
  DatabaseIcon,
  FileCodeIcon,
  FlaskConicalIcon,
  GlobeIcon,
  LayersIcon,
  MapIcon,
  MousePointerClickIcon,
  PenLineIcon,
  PenToolIcon,
  SearchCheckIcon,
  ServerIcon,
  SparklesIcon,
  TargetIcon,
  TypeIcon,
  ZapIcon,
  SendIcon,
  WalletIcon,
  TrendingUpIcon,
  ClockIcon,
  UsersIcon,
  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,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
} from "@/components/ui/sheet";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";

type Employee = {
  id: number;
  name: string;
  email: string;
  avatar: string;
  role: string;
  department: string;
  status: "Active" | "On Leave" | "Remote";
  location: string;
  joined: string;
  salary: number;
  performance: number;
  skills: string[];
  about: string;
};

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

const employees: Employee[] = [
  { id: 1, name: "Sunil Joshi", email: "sunil@example.com", avatar: `${AVATAR}1.webp`, role: "Senior Designer", department: "Design", status: "Active", location: "New York", joined: "Mar 12, 2022", salary: 82000, performance: 88, skills: ["Figma", "Prototyping", "Design systems"], about: "Leads the design system and owns the dashboard UI kit across web and mobile." },
  { id: 2, name: "Andrew McDownland", email: "andrew@example.com", avatar: `${AVATAR}8.webp`, role: "Staff Engineer", department: "Engineering", status: "Remote", location: "London", joined: "Jan 04, 2021", salary: 112000, performance: 92, skills: ["TypeScript", "Next.js", "PostgreSQL"], about: "Owns the platform architecture and mentors the frontend engineering team." },
  { id: 3, name: "Christopher Jamil", email: "chris@example.com", avatar: `${AVATAR}2.webp`, role: "Backend Engineer", department: "Engineering", status: "Active", location: "New York", joined: "Aug 22, 2022", salary: 104000, performance: 76, skills: ["Node.js", "Redis", "AWS"], about: "Builds the billing and notification services and keeps the API fast." },
  { id: 4, name: "Nirav Joshi", email: "nirav@example.com", avatar: `${AVATAR}3.webp`, role: "Content Manager", department: "Marketing", status: "On Leave", location: "Berlin", joined: "Nov 02, 2023", salary: 67000, performance: 64, skills: ["SEO", "Copywriting", "Analytics"], about: "Plans the editorial calendar and manages the blog and newsletter." },
  { id: 5, name: "Priya Shah", email: "priya@example.com", avatar: `${AVATAR}4.webp`, role: "Product Designer", department: "Design", status: "Remote", location: "Berlin", joined: "May 17, 2023", salary: 78000, performance: 81, skills: ["Research", "UX writing", "Figma"], about: "Runs user research and turns findings into shipped product improvements." },
  { id: 6, name: "Daniel Kim", email: "daniel@example.com", avatar: `${AVATAR}5.webp`, role: "Engineering Manager", department: "Engineering", status: "Active", location: "London", joined: "Feb 08, 2020", salary: 118000, performance: 95, skills: ["Leadership", "Roadmaps", "React"], about: "Manages two squads and drives quarterly planning with product." },
  { id: 7, name: "Maria Lopez", email: "maria@example.com", avatar: `${AVATAR}6.webp`, role: "Growth Marketer", department: "Marketing", status: "Active", location: "New York", joined: "Jul 29, 2022", salary: 71000, performance: 73, skills: ["Paid ads", "CRO", "Email"], about: "Owns acquisition experiments and lifecycle email campaigns." },
];

const DEPARTMENTS: Record<
  string,
  { icon: LucideIcon; text: string; dot: string; cover: string; chip: string }
> = {
  Design: { icon: PaletteIcon, text: "text-teal-400", dot: "bg-teal-400", cover: "from-teal-400/40 via-sky-400/25 to-transparent", chip: "bg-teal-400/10! text-teal-400!" },
  Engineering: { icon: CodeIcon, text: "text-blue-500", dot: "bg-blue-500", cover: "from-blue-500/40 via-sky-400/25 to-transparent", chip: "bg-blue-500/10! text-blue-500!" },
  Marketing: { icon: MegaphoneIcon, text: "text-orange-400", dot: "bg-orange-400", cover: "from-orange-400/40 via-amber-300/25 to-transparent", chip: "bg-orange-400/10! text-orange-400!" },
};

function getTierStroke(score: number) {
  if (score >= 90) return "stroke-chart-2";
  if (score >= 80) return "stroke-blue-500";
  if (score >= 70) return "stroke-orange-400";
  return "stroke-destructive";
}

const STATUS_DOTS: Record<Employee["status"], string> = {
  Active: "bg-chart-2",
  "On Leave": "bg-orange-400",
  Remote: "bg-blue-500",
};

const SKILL_ICONS: Record<string, LucideIcon> = {
  Figma: PenToolIcon,
  Prototyping: MousePointerClickIcon,
  "Design systems": LayersIcon,
  TypeScript: FileCodeIcon,
  "Next.js": GlobeIcon,
  PostgreSQL: DatabaseIcon,
  "Node.js": ServerIcon,
  Redis: ZapIcon,
  AWS: CloudIcon,
  SEO: SearchCheckIcon,
  Copywriting: PenLineIcon,
  Analytics: ActivityIcon,
  Research: FlaskConicalIcon,
  "UX writing": TypeIcon,
  Leadership: UsersIcon,
  Roadmaps: MapIcon,
  React: AtomIcon,
  "Paid ads": MegaphoneIcon,
  CRO: TargetIcon,
  Email: MailIcon,
};

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

export default function MasterDetailTable() {
  const [search, setSearch] = useState("");
  const [activeId, setActiveId] = useState<number | null>(null);
  const [department, setDepartment] = useState<string>("All");
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    const t = setTimeout(() => setMounted(true), 150);
    return () => clearTimeout(t);
  }, []);

  const rows = useMemo(() => {
    const q = search.trim().toLowerCase();
    return employees.filter(
      (e) =>
        (department === "All" || e.department === department) &&
        (!q ||
        e.name.toLowerCase().includes(q) ||
        e.role.toLowerCase().includes(q) ||
        e.department.toLowerCase().includes(q)),
    );
  }, [search, department]);

  const active = employees.find((e) => e.id === activeId) ?? null;

  return (
    <section className="py-8 px-4 max-w-4xl 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>Team Directory</CardTitle>
            <CardDescription>
              Click a row to open the full employee profile
            </CardDescription>
          </div>
          <CardAction>
            <InputGroup className="w-56 dark:bg-background dark:border-border">
              <InputGroupAddon align="inline-start">
                <SearchIcon className="size-4" />
              </InputGroupAddon>
              <InputGroupInput
                value={search}
                onChange={(e) => setSearch(e.target.value)}
                placeholder="Search team..."
              />
            </InputGroup>
          </CardAction>
        </CardHeader>

        <div className="border-t border-border">
          <Tabs
            value={department}
            onValueChange={(v) => setDepartment(v as string)}
            className="w-full"
          >
            <TabsList
              variant="line"
              className="flex w-full border-b border-border bg-transparent p-0! rounded-none h-fit! gap-0! justify-start!"
            >
              {["All", ...Object.keys(DEPARTMENTS)].map((name) => {
                const Icon = name === "All" ? UsersIcon : DEPARTMENTS[name].icon;
                const count =
                  name === "All"
                    ? employees.length
                    : employees.filter((e) => e.department === name).length;
                const isActive = department === name;
                return (
                  <TabsTrigger
                    key={name}
                    value={name}
                    className={cn(
                      "relative flex items-center justify-center cursor-pointer text-sm font-medium transition-colors outline-none whitespace-nowrap bg-transparent",
                      "h-auto! p-0! flex-none!",
                      "data-active:bg-transparent! dark:data-active:bg-transparent! dark:data-active:border-transparent!",
                      "border-transparent data-active:border-transparent shadow-none data-active:shadow-none after:hidden",
                      isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground",
                    )}
                  >
                    <span className="flex items-center gap-2 px-4 py-3 rounded-md transition-colors hover:bg-muted/70">
                      <Icon className="w-4 h-4" />
                      <span>{name}</span>
                      <span className="text-xs text-muted-foreground">{count}</span>
                    </span>
                    
                    <span
                      className={cn(
                        "pointer-events-none absolute bottom-[-1px] left-0 right-0 h-[2px] bg-primary transition-opacity duration-200",
                        isActive ? "opacity-100" : "opacity-0",
                      )}
                    />
                  </TabsTrigger>
                );
              })}
            </TabsList>
          </Tabs>
        </div>

        <div className="overflow-x-auto min-h-[30rem]">
          <Table className="min-w-full">
            <TableHeader>
              <TableRow>
                <TableHead className="text-base font-medium py-3 text-left border-b border-border px-4 h-auto">
                  Employee
                </TableHead>
                <TableHead className="text-base font-medium py-3 text-left border-b border-border px-4 h-auto">
                  Department
                </TableHead>
                <TableHead className="text-base font-medium py-3 text-left border-b border-border px-4 h-auto">
                  Location
                </TableHead>
                <TableHead className="text-base font-medium py-3 text-left border-b border-border px-4 h-auto">
                  Status
                </TableHead>
                <TableHead className="hidden md:table-cell text-base font-medium py-3 text-left border-b border-border px-4 h-auto">
                  Performance
                </TableHead>
                <TableHead className="w-10 border-b border-border" />
              </TableRow>
            </TableHeader>
            <TableBody className="divide-y divide-border">
              {rows.length === 0 && (
                <TableRow>
                  <TableCell
                    colSpan={6}
                    className="h-24 text-center text-muted-foreground"
                  >
                    No team members found.
                  </TableCell>
                </TableRow>
              )}
              {rows.map((row) => (
                <TableRow
                  key={row.id}
                  className={cn(
                    "group/row cursor-pointer transition-colors",
                    activeId === row.id ? "bg-muted/60" : "hover:bg-muted/40",
                  )}
                  onClick={() => setActiveId(row.id)}
                >
                  <TableCell className="whitespace-nowrap py-3 px-4">
                    <div className="flex items-center gap-3">
                      <div className="relative">
                        <Avatar>
                          <AvatarImage src={row.avatar} alt={row.name} />
                          <AvatarFallback>{row.name[0]}</AvatarFallback>
                        </Avatar>
                        <span className="absolute -right-0.5 -bottom-0.5 flex size-3">
                          {row.status === "Active" && (
                            <span
                              className={cn(
                                "absolute inline-flex size-full animate-ping rounded-full opacity-60",
                                STATUS_DOTS[row.status],
                              )}
                            />
                          )}
                          <span
                            className={cn(
                              "relative inline-flex size-3 rounded-full border-2 border-card",
                              STATUS_DOTS[row.status],
                            )}
                          />
                        </span>
                      </div>
                      <div className="flex flex-col">
                        <h6 className="text-sm font-medium">{row.name}</h6>
                        <span className="text-xs text-muted-foreground">
                          {row.role}
                        </span>
                      </div>
                    </div>
                  </TableCell>
                  <TableCell className="whitespace-nowrap py-3 px-4">
                    {(() => {
                      const d = DEPARTMENTS[row.department];
                      const Icon = d.icon;
                      return (
                        <span
                          className={cn(
                            "inline-flex items-center gap-2 font-medium",
                            d.text,
                          )}
                        >
                          <Icon className="size-4" />
                          {row.department}
                        </span>
                      );
                    })()}
                  </TableCell>
                  <TableCell className="whitespace-nowrap py-3 px-4 text-sm text-foreground">
                    <span className="inline-flex items-center gap-2">
                      <MapPinIcon className="size-4 text-muted-foreground" />
                      {row.location}
                    </span>
                  </TableCell>
                  <TableCell className="whitespace-nowrap py-3 px-4">
                    <Badge variant="outline" className="gap-2">
                      <span
                        className={cn("size-2 rounded-full", STATUS_DOTS[row.status])}
                      />
                      {row.status}
                    </Badge>
                  </TableCell>
                  <TableCell className="hidden md:table-cell py-3 px-4">
                    <div className="flex items-center gap-2.5">
                      <svg viewBox="0 0 36 36" className="size-7 -rotate-90">
                        <circle
                          cx="18"
                          cy="18"
                          r="14"
                          fill="none"
                          strokeWidth="4"
                          className="stroke-muted"
                        />
                        <circle
                          cx="18"
                          cy="18"
                          r="14"
                          fill="none"
                          strokeWidth="4"
                          strokeLinecap="round"
                          pathLength={100}
                          strokeDasharray={100}
                          strokeDashoffset={mounted ? 100 - row.performance : 100}
                          className={cn(
                            "transition-[stroke-dashoffset] duration-1000 ease-out",
                            getTierStroke(row.performance),
                          )}
                        />
                      </svg>
                      <span className="text-sm text-foreground">{row.performance}%</span>
                    </div>
                  </TableCell>
                  <TableCell className="py-3 px-4">
                    <span
                      className={cn(
                        "flex size-7 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors group-hover/row:bg-muted",
                        activeId === row.id && "border-foreground/30 bg-muted text-foreground",
                      )}
                    >
                      <ChevronRightIcon
                        className={cn(
                          "size-4 transition-transform duration-300 ease-out group-hover/row:translate-x-0.5",
                          activeId === row.id && "rotate-90 group-hover/row:translate-x-0",
                        )}
                      />
                    </span>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </div>
      </Card>

      <Sheet
        open={active !== null}
        onOpenChange={(open) => {
          if (!open) setActiveId(null);
        }}
      >
        <SheetContent className="w-full gap-0 overflow-y-auto p-0 sm:max-w-md">
          {active &&
            (() => {
              const dept = DEPARTMENTS[active.department];
              const DeptIcon = dept.icon;
              const years = Math.max(
                0,
                (Date.now() - new Date(active.joined).getTime()) /
                  (365.25 * 24 * 60 * 60 * 1000),
              );
              const stats = [
                { label: "Performance", value: `${active.performance}%`, icon: TrendingUpIcon },
                { label: "Salary", value: `$${Math.round(active.salary / 1000)}k`, icon: WalletIcon },
                { label: "Tenure", value: `${years.toFixed(1)} yrs`, icon: ClockIcon },
              ];
              const contacts = [
                { icon: MailIcon, label: "Email", value: active.email },
                { icon: MapPinIcon, label: "Location", value: active.location },
                { icon: CalendarIcon, label: "Joined", value: active.joined },
              ];
              return (
                <>
                  <div
                    className={cn(
                      "relative h-28 shrink-0 bg-gradient-to-br",
                      dept.cover,
                    )}
                  >
                    <div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,var(--color-foreground)_1px,transparent_0)] bg-[size:14px_14px] opacity-[0.06]" />
                  </div>

                  <div className="-mt-10 flex flex-col gap-4 px-5 pb-5">
                    <div className="relative w-fit">
                      <Avatar className="size-20 ring-4 ring-background">
                        <AvatarImage src={active.avatar} alt={active.name} />
                        <AvatarFallback>{active.name[0]}</AvatarFallback>
                      </Avatar>
                      <span
                        className={cn(
                          "absolute right-0.5 bottom-0.5 size-4 rounded-full border-[3px] border-background",
                          STATUS_DOTS[active.status],
                        )}
                      />
                    </div>

                    <div className="flex flex-col gap-2">
                      <div>
                        <SheetTitle className="text-xl">{active.name}</SheetTitle>
                        <SheetDescription>{active.role}</SheetDescription>
                      </div>
                      <div className="flex flex-wrap items-center gap-2">
                        <Badge variant="secondary" className={cn("gap-1.5", dept.chip)}>
                          <DeptIcon className="size-3.5" />
                          {active.department}
                        </Badge>
                        <Badge variant="outline" className="gap-2">
                          <span
                            className={cn("size-2 rounded-full", STATUS_DOTS[active.status])}
                          />
                          {active.status}
                        </Badge>
                      </div>
                    </div>

                    <div className="grid grid-cols-3 gap-3">
                      {stats.map((st) => {
                        const Icon = st.icon;
                        return (
                          <Card key={st.label} size="sm" className="gap-1 ring-0 border border-border">
                            <CardContent className="flex flex-col gap-1">
                              <Icon className={cn("size-4", dept.text)} />
                              <span className="text-base font-semibold">{st.value}</span>
                              <span className="text-xs text-muted-foreground">{st.label}</span>
                            </CardContent>
                          </Card>
                        );
                      })}
                    </div>

                    <div className="flex flex-col gap-1.5">
                      <span className="text-sm font-medium">About</span>
                      <p className="text-sm text-muted-foreground">{active.about}</p>
                    </div>

                    <Card size="sm" className="ring-0 border border-border">
                      <CardContent className="flex flex-col gap-3">
                        {contacts.map((c) => {
                          const Icon = c.icon;
                          return (
                            <div key={c.label} className="flex items-center gap-3">
                              <Icon className="size-5 shrink-0 text-muted-foreground" />
                              <div className="flex flex-col">
                                <span className="text-xs text-muted-foreground">{c.label}</span>
                                <span className="text-sm font-medium">{c.value}</span>
                              </div>
                            </div>
                          );
                        })}
                      </CardContent>
                    </Card>

                    <div className="flex flex-col gap-2">
                      <span className="text-sm font-medium">Skills</span>
                      <div className="flex flex-wrap gap-2">
                        {active.skills.map((skill) => {
                          const SkillIcon = SKILL_ICONS[skill] ?? SparklesIcon;
                          return (
                            <Badge key={skill} variant="outline" className="gap-1.5">
                              <SkillIcon className={cn("size-3.5", dept.text)} />
                              {skill}
                            </Badge>
                          );
                        })}
                      </div>
                    </div>
                  </div>

                  <div className="sticky bottom-0 mt-auto flex gap-2 border-t border-border bg-background p-4">
                    <Button className="flex-1 cursor-pointer">
                      <SendIcon /> Send message
                    </Button>
                    <Button
                      variant="outline"
                      className="cursor-pointer"
                      onClick={() => setActiveId(null)}
                    >
                      Close
                    </Button>
                  </div>
                </>
              );
            })()}
        </SheetContent>
      </Sheet>
    </section>
  );
}
```
