# Table With Row Selection and Bulk Actions

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/table-with-row-selection-and-bulk-actions).

- Page, with a live preview: https://shadcn.exchange/r/weinc/table-with-row-selection-and-bulk-actions
- Screenshot: https://shadcn.exchange/shot/weinc/table-with-row-selection-and-bulk-actions/light.png
- Registry item: https://we.inc/r/table-with-row-selection-and-bulk-actions.json
- Listed: 2026-10-29, source last changed 2026-10-29

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/table-with-row-selection-and-bulk-actions.json
```

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

## Files

### `components/we-inc/table-with-row-selection-and-bulk-actions.tsx`

```tsx
import { useEffect, useRef, useState } from "react";

const initial = [
  { id: 1, name: "Priya Natarajan", email: "priya@fieldnote.co", plan: "Annual", joined: "12 Mar 2026" },
  { id: 2, name: "Owen Gallagher", email: "owen.g@harbourmail.net", plan: "Monthly", joined: "2 Apr 2026" },
  { id: 3, name: "Sofia Lindqvist", email: "sofia@lindqvist.se", plan: "Free", joined: "19 May 2026" },
  { id: 4, name: "Kwame Asante", email: "kwame@asante.studio", plan: "Annual", joined: "7 Jul 2026" },
  { id: 5, name: "Maud Fischer", email: "maud.fischer@postbox.de", plan: "Monthly", joined: "30 Aug 2026" },
];

export default function TableWithRowSelectionAndBulkActions() {
  const [rows, setRows] = useState(initial);
  const [picked, setPicked] = useState<Set<number>>(() => new Set([2, 4]));
  const all = useRef<HTMLInputElement>(null);
  const count = picked.size;
  const allOn = count > 0 && count === rows.length;

  useEffect(() => {
    if (all.current) all.current.indeterminate = count > 0 && !allOn;
  }, [count, allOn]);

  const flip = (id: number) =>
    setPicked((p) => {
      const n = new Set(p);
      if (n.has(id)) n.delete(id);
      else n.add(id);
      return n;
    });
  const archive = () => {
    setRows((r) => r.filter((x) => !picked.has(x.id)));
    setPicked(new Set());
  };

  return (
    <section className="bg-rose-50/60 px-5 py-14 sm:py-20">
      <div className="mx-auto max-w-4xl">
        <div className="overflow-hidden rounded-2xl border border-rose-100 bg-white shadow-sm">
          <div className="flex min-h-[64px] flex-wrap items-center justify-between gap-3 px-5 py-3" aria-live="polite">
            {count === 0 ? (
              <p className="text-sm text-slate-600">{rows.length} subscribers. Tick rows to act on them.</p>
            ) : (
              <>
                <p className="text-sm font-medium text-slate-900">{count} selected</p>
                <div className="flex gap-2">
                  <button type="button" className="h-10 rounded-lg border border-slate-300 px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-600">Export CSV</button>
                  <button type="button" onClick={archive} className="h-10 rounded-lg bg-rose-700 px-3.5 text-sm font-medium text-white hover:bg-rose-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-600 focus-visible:ring-offset-2">Archive {count}</button>
                </div>
              </>
            )}
          </div>
          <div className="overflow-x-auto">
            <table className="w-full min-w-[600px] border-collapse text-left text-sm">
              <caption className="sr-only">Newsletter subscribers for The Saturday Loaf</caption>
              <thead className="border-y border-slate-200 bg-slate-50 text-slate-600">
                <tr>
                  <th scope="col" className="w-12 px-5 py-3">
                    <input ref={all} type="checkbox" aria-label="Select all subscribers" checked={allOn} onChange={() => setPicked(allOn ? new Set() : new Set(rows.map((r) => r.id)))} className="h-4 w-4 accent-rose-700" />
                  </th>
                  <th scope="col" className="px-3 py-3 font-medium">Name</th>
                  <th scope="col" className="px-3 py-3 font-medium">Email</th>
                  <th scope="col" className="px-3 py-3 font-medium">Plan</th>
                  <th scope="col" className="px-5 py-3 font-medium">Joined</th>
                </tr>
              </thead>
              <tbody className="divide-y divide-slate-100">
                {rows.map((r) => {
                  const on = picked.has(r.id);
                  return (
                    <tr key={r.id} className={on ? "bg-rose-50" : "hover:bg-slate-50"}>
                      <td className="px-5 py-3.5">
                        <input type="checkbox" aria-label={`Select ${r.name}`} checked={on} onChange={() => flip(r.id)} className="h-4 w-4 accent-rose-700" />
                      </td>
                      <th scope="row" className="px-3 py-3.5 font-medium text-slate-900">{r.name}</th>
                      <td className="px-3 py-3.5 text-slate-600">{r.email}</td>
                      <td className="px-3 py-3.5 text-slate-700">{r.plan}</td>
                      <td className="px-5 py-3.5 text-slate-600">{r.joined}</td>
                    </tr>
                  );
                })}
                {rows.length === 0 && (
                  <tr><td colSpan={5} className="px-5 py-10 text-center text-slate-600">Everyone is archived. Reload to bring them back.</td></tr>
                )}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </section>
  );
}
```
