Inline row actions

A per-row ⋮ menu with edit, duplicate, and delete. Destructive actions show a confirmation modal before committing. State is updated optimistically — no network call needed to see the result.

Inline row actions

Click the ⋮ menu on any row to edit, duplicate, or delete it.

Name
Department
Role
Aria Chen
Engineering
Engineering Lead
Marcus Webb
Product
Product Manager
Priya Kapoor
Design
Senior Designer
Jordan Ellis
Analytics
Data Scientist
Sam Rivera
Engineering
DevOps Engineer
Taylor Brooks
Sales
Account Manager

This demo uses Tailwind utility classes for layout — swap in your own CSS when copying into your project.

The actions column

Use button: true on the column so clicks don't bubble toonRowClicked, and keep the width tight so it doesn't crowd other columns:

{
  name: '',
  button: true,
  width: '48px',
  cell: row => <RowMenu row={row} onEdit={openEdit} onDelete={openDelete} />,
}

Closing the dropdown on outside click

Track open state per-row inside the menu component and close on a documentmousedown that falls outside the ref:

function RowMenu({ row, onEdit, onDelete }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    if (!open) return;
    const handle = e => {
      if (ref.current && !ref.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener('mousedown', handle);
    return () => document.removeEventListener('mousedown', handle);
  }, [open]);

  return (
    <div ref={ref}>
      <button onClick={e => { e.stopPropagation(); setOpen(o => !o); }}></button>
      {open && (
        <div className="dropdown">
          <button onClick={() => { setOpen(false); onEdit(row); }}>Edit</button>
          <button onClick={() => { setOpen(false); onDelete(row); }}>Delete</button>
        </div>
      )}
    </div>
  );
}

Confirmation before delete

Keep a modal state at the table level. The menu sets it; the modal reads it and calls the commit function on confirm:

type Modal = { type: 'none' } | { type: 'delete'; row: Employee };

const [modal, setModal] = useState<Modal>({ type: 'none' });

function commitDelete(row: Employee) {
  setData(prev => prev.filter(r => r.id !== row.id));
  setModal({ type: 'none' });
}

{modal.type === 'delete' && (
  <ConfirmDialog
    message={`Delete ${modal.row.name}?`}
    onConfirm={() => commitDelete(modal.row)}
    onCancel={() => setModal({ type: 'none' })}
  />
)}