DEV Community

Cover image for Build Google Sheets-Style Collaborative Comments in Your Web App Using TanStack Tables and Velt SDK🔥🤝🚀
Astrodevil
Astrodevil

Posted on • Edited on

8 5 5 5 5

Build Google Sheets-Style Collaborative Comments in Your Web App Using TanStack Tables and Velt SDK🔥🤝🚀

TLDR;

Adding real-time collaboration to your web app doesn’t have to be hard. In this guide, we’ll show you how we built Google Sheets-style live commenting using Velt SDK.

Features include:

  • Real-time comments directly on table cells (just like Google Sheets).

  • Seamless sync and presence tracking without complex backend work.

  • A dynamic UI built with Tanstack Table and Next.js.

By the end, you’ll have the foundation to create collaborative, engaging tools. You can take it further by adding reactions, notifications or user roles.

Here is a preview of what you will build:

Velt Google Spreadsheet

Let’s dive in! 🚀

start gif


Introduction

Real-time features make any platform feel alive and collaborative. One of the most impactful is live commenting, where users can share feedback or chat instantly on shared documents or resources. It’s a simple way to boost engagement, keep conversations flowing, and help teams stay in sync.

For editors or project owners, it’s also a chance to interact more directly with collaborators and make teamwork smoother.

But building a system like this from scratch isn’t easy, you’ll need to manage real-time UI updates, user presence tracking and efficient data handling. In this guide, I’ll show you how to use Velt SDK to handle all of that and build a live commenting feature with a Google Sheets-style collaborative experience.

In this guide, you’ll learn how to add live commenting to a Google Sheets-style application built with Tanstack React Tables. We’ll integrate Velt Comments to allow users to leave popover comments directly on table cells - just like in Google Sheets.

Here’s what we’ll walk through:

  • Setting up a React Tables frontend using Next.js and React Tables

  • Adding live comments to the app with Velt Comments

Challenges of Building Real-Time Collaboration

Modern web development has grown increasingly complex. We’ve moved from simple HTML, CSS, and JavaScript to large-scale systems with demanding features. Real-time collaboration, once a niche requirement, now requires extensive planning and technical workarounds.

Many developers assume complex systems must be built from scratch. While this may seem efficient at first, it often leads to rigid architectures that become harder to maintain over time.

Frontend

  • Real-time syncing, like in Google Sheets, involves complex state management, polling, and WebSockets.

  • UI elements such as live comments, version tracking and real-time cursors must feel seamless and intuitive.

  • Performance optimization is critical to prevent lag, requiring efficient DOM updates and asset loading strategies.

Backend

  • Handling concurrent users requires database optimization, load balancing, and distributed systems.

  • Ensuring data consistency demands sophisticated algorithms like Operational Transform (OT) or Conflict-Free Replicated Data Types (CRDTs).

  • Preventing downtime and data loss requires a 24/7 infrastructure with redundancy and failover mechanisms.

How Velt SDK Simplifies this Process?

Velt SDK is designed to embed powerful collaborative features like commenting and recording functionalities into SaaS products without too much manual coding. If you’re a builder inspired by collaborative giants like Google Docs and Figma, Velt will enable you to add features such as inline comments, live reactions, real-time presence indicators inside your app without extensive development overhead.

The Velt SDK offers:

  • Pre-built UI Components: Ready-to-use features like live commenting and real-time cursors.

  • Cross-Platform Compatibility: Ensures seamless operation across various browsers and devices.

  • Robust Infrastructure: Manages WebSocket connections, scalability, and data synchronization.

  • Scalability and Reliability: Provides uptime guarantees and a global server network.

By leveraging Velt, developers can efficiently incorporate advanced collaboration tools, allowing them to focus on enhancing their core product offerings.

Prerequisites

Before we dive into building our collaborative spreadsheet, let’s set up the development environment using a pre-built starter template. This template provides a minimal foundation, allowing us to focus on integrating real-time collaboration step by step.

What’s in the Starter Template?

The starter template includes:

  • A simplified SpreadsheetPage that provides a basic layout with placeholder content.

  • A Header component with essential UI elements like the document title and user switcher.

  • Routing setup to navigate between different spreadsheets.

What we are going to add:

Throughout the article, we will integrate features like live user presence, a powerful table and live comments.

Setting up the project:

  1. Clone the repository

    git@github.com:Studio1HQ/google-sheets.git
    cd google-sheets
    
  2. Install dependencies and start development server

    pnpm install
    
    pnpm dev
    
  3. Open the app in your browser and navigate to:

    http://localhost:3000
    

You should see a basic page with:

  • A list of available sheets – Each sheet represents a structured dataset you can open.

  • A search bar or navigation options – Helps you quickly find a specific sheet.

  • Static User indicators – Shows user we will use to test Velt collab features since we are not adding authentication to this setup.

  • Document ID – A unique identifier for reference.

When you click on a sheet, you’ll be taken to a new dynamic page displaying its content:

velt demo page

This is our starting point. In the next sections, we’ll progressively add real-time functionality, turning this simple interface into a fully collaborative experience.

Section 1: Adding TanStack React Interactive Table

In this section, you will change the simple Spreadsheetpage.tsx from the simple structure to a fully interactive Tanstack React Table designed in a google sheets style with editable cells and pagination.

Tan-stack uses some necessary imports that are essential for functionality such as table rendering, pagination and input handling:

Setting Up the Required Imports

import React, { useRef, useEffect, useState } from "react";
import {
  Table,
  ColumnDef,
  useReactTable,
  getCoreRowModel,
  getPaginationRowModel,
  flexRender,
  RowData,
} from "@tanstack/react-table";
import { useParams, useSearchParams } from "next/navigation";
import { useSkipper } from "@/lib/hooks";
import { debounce } from "lodash";
import { Cell, getColumnLabel, range } from "@/lib/sheetData";
Enter fullscreen mode Exit fullscreen mode

Extending @tanstack/react-table for Data Updates

You need to extend @tanstack/react-table to allow inline cell editing. Add the following type extension just below the imports we have added.

declare module "@tanstack/react-table" {
  interface TableMeta<TData extends RowData> {
    updateData: (rowIndex: number, columnId: string, value: Cell) => void;
  }
}
Enter fullscreen mode Exit fullscreen mode

Implementing the Editable Cell Component

The EditableCell component allows users to edit cell values inline. This component manages focus state and updates the table when changes are made.

This should also be added just below the extended type.

function EditableCell<TData extends Record<string, Cell>>({
  getValue,
  row,
  column,
  table,
}: {
  getValue: () => unknown;
  row: { index: number };
  column: { id: string };
  table: Table<TData>;
}) {
  const initialValue = (getValue() as Cell)?.value || "";
  const [value, setValue] = useState(initialValue);

  const onBlur = () => {
    table.options.meta?.updateData(
      row.index,
      column.id,
      value as unknown as Cell
    );
  };

  useEffect(() => {
    setValue(initialValue);
  }, [initialValue]);

  return (
    <div className="relative w-full h-full">
      <input
        value={value as string}
        onChange={(e) => setValue(e.target.value)}
        onBlur={onBlur}
        className="w-full h-full border-none focus:outline-none bg-transparent px-1 py-0"
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Configuring the Spreadsheet Table Component Logic

This is the upper part in our react component where we usually define logic like hooks, state, ref etc.

We will add:

Constants: Define important settings for the spreadsheet:

  • COLUMNS_COUNT: Number of columns.
  • CELL_WIDTH: Fixed width for each cell.
const COLUMNS_COUNT = 10;
const CELL_WIDTH = 600;
Enter fullscreen mode Exit fullscreen mode

Refs and Hooks: Set up necessary references and routing hooks:

const searchParams = useSearchParams();
const urlTitle = searchParams.get("title");

const measureRef = useRef<HTMLSpanElement>(null);
const gridRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
Enter fullscreen mode Exit fullscreen mode

Default Column Definition: Configure the default column settings with the editable cell:

const defaultColumn: Partial<ColumnDef<Record<string, Cell>>> = {
cell: (props) => <EditableCell<Record<string, Cell>> {...props} />,
size: CELL_WIDTH,
};
Enter fullscreen mode Exit fullscreen mode

Column Definitions:

  • Uses range(COLUMNS_COUNT) to dynamically create column headers (A, B, C...).
  • Each column has a fixed width.
const columns = React.useMemo<ColumnDef<Record<string, Cell>>[]>(() => {
      return range(COLUMNS_COUNT).map((i) => {
        const columnLabel = getColumnLabel(i);
        return {
          accessorKey: columnLabel,
          header: () => <div className="text-center">{columnLabel}</div>,
          size: CELL_WIDTH,
        };
      });
}, []);
Enter fullscreen mode Exit fullscreen mode

State Management: Three state variables

  • data: Holds spreadsheet rows.
  • filename: Stores spreadsheet title.
  • autoResetPageIndex: Used to manage page updates.
const [data, setData] = useState(initialData);
const [filename, setFilename] = useState(initialTitle);
const [autoResetPageIndex, skipAutoResetPageIndex] = useSkipper();
Enter fullscreen mode Exit fullscreen mode

Title Sync with URL

useEffect(() => {
if (urlTitle) {
setFilename(urlTitle);
}
}, [urlTitle, setFilename]);
Enter fullscreen mode Exit fullscreen mode

Implement Data Update Handling

This function:

  • Updates the row data dynamically.
  • Uses debounce to avoid unnecessary re-renders.
const updateData = debounce(
      (rowIndex: number, columnId: string, value: Cell) => {
        skipAutoResetPageIndex();
        setData((old) =>
          old.map((row, index) => {
            if (index === rowIndex) {
              return {
                ...old[rowIndex]!,
                [columnId]: value as Cell,
              } as Record<string, Cell>;
            }
            return row;
          })
        );
      },
      300
);
Enter fullscreen mode Exit fullscreen mode

The Table Instance: This is the core of the table functionality:

  • Uses useReactTable() to create a table instance.
  • Sets up pagination and core row models.
  • Links updateData to the table meta.
const table = useReactTable({
    data,
    columns,
    defaultColumn,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
      initialState: {
        pagination: {
          pageSize: 20,
        },
      },
      autoResetPageIndex,
      meta: {
        updateData,
      },
      debugTable: true,
      debugHeaders: true,
      debugColumns: true,
    });
Enter fullscreen mode Exit fullscreen mode

Rendering the Table UI

Finally, add the JSX to render the complete spreadsheet interface, including headers, row numbers, and pagination controls

Finally, add the JSX to render the complete spreadsheet interface, including headers, row numbers, and pagination controls:

return (
  <div className="h-screen w-screen flex flex-col bg-white text-xs">
    <Header
      type="sheets"
      measureRef={measureRef}
      filename={urlTitle || filename}
      inputRef={inputRef}
      setFilename={setFilename}
      handleTitleFocus={handleTitleFocus}
    />

    <div className="flex border-b border-gray-200 sticky top-0 bg-gray-100 z-10">
      <div className="w-10 flex items-center justify-center border-r border-gray-200 bg-gray-100 text-gray-500">
        #
      </div>
      <div className="flex-1 overflow-x-auto">
        <div className="flex">
          {table.getFlatHeaders().map((header) => (
            <div
              key={header.id}
              style={{ width: `${header.getSize()}px` }}
              className="border-r border-gray-200 h-6 flex items-center justify-center bg-gray-100 text-gray-700 font-medium"
            >
              {flexRender(
                header.column.columnDef.header,
                header.getContext()
              )}
            </div>
          ))}
          <div className="w-full h-6 bg-gray-100"></div>
        </div>
      </div>
    </div>

    <div className="flex-1 overflow-auto" ref={gridRef}>
      <div className="flex">
        <div className="w-10 sticky left-0 bg-gray-100 z-10">
          {table.getRowModel().rows.map((row, i) => (
            <div
              key={row.id}
              className="h-6 border-b border-r border-gray-200 flex items-center justify-center text-gray-500"
            >
              {table.getState().pagination.pageIndex *
                table.getState().pagination.pageSize +
                i +
                1}
            </div>
          ))}
        </div>
        <div className="flex-1 overflow-x-auto">
          <div>
            {table.getRowModel().rows.map((row) => (
              <div key={row.id} className="flex border-b border-gray-200">
                {row.getVisibleCells().map((cell) => (
                  <div
                    key={cell.id}
                    style={{ width: `${cell.column.getSize()}px` }}
                    className="border-r border-gray-200 h-6 overflow-hidden"
                  >
                    {flexRender(
                      cell.column.columnDef.cell,
                      cell.getContext()
                    )}
                  </div>
                ))}
                <div className="w-full h-6"></div>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>

    <div className="border-t border-gray-200 py-2 px-4 flex items-center justify-between bg-gray-50">
      <div className="flex items-center gap-2">
        <button
          className="border rounded p-1 text-xs disabled:opacity-50"
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}
        >
          Previous
        </button>
        <button
          className="border rounded p-1 text-xs disabled:opacity-50"
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}
        >
          Next
        </button>
        <span className="text-xs">
          Page {table.getState().pagination.pageIndex + 1} of{" "}
          {table.getPageCount()}
        </span>
      </div>
      <span className="text-xs">{table.getRowModel().rows.length} rows</span>
    </div>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

exciting gif


Section 2: Add Live Commenting and User Identification with Velt.

In this section, we will enhance our google sheets sample by adding Velt features

Install and Configure Velt SDK.

Velt requires an API key to function. Create a .env.development.local file at the root of your Next.js project and Add your Velt API key which you can obtain from Velt’s Console Dashboard

NEXT_PUBLIC_VELT_ID=your-velt-api-key-here
Enter fullscreen mode Exit fullscreen mode

Important Notes on Using the Velt API Key

  • The free-tier API key works only for local development. Velt automatically includes http://localhost:3000 in the allowed domains.

  • To interact with live preview of your app, add your domain (e.g., yourapp.com, *.yourapp.com) in the Velt Console under API Keys or Settings.

  • If deploying without updating the allowed domains, the Velt SDK will fail to initialize, and features like comments and presence will not function.

  • To add domains, log in to the Velt Console, navigate to API Keys or Project Settings, find the Allowed Domains section, add your production domain, and save.

  • Test on localhost first, then verify domain settings before going live to prevent disruptions.

Add Required Imports to Root Layout file

  • "use client": Marks this as a Client Component since Velt requires client-side functionality.

  • VeltProvider: The Velt context provider to enable collaboration features.

"use client";
import { VeltProvider } from "@veltdev/react";
Enter fullscreen mode Exit fullscreen mode

Wrap the Layout with VeltProvider

Modify the RootLayout component to wrap its content with VeltProvider. Update the return statement like this:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <VeltProvider apiKey={process.env.NEXT_PUBLIC_VELT_ID || ""}>
          {children}
        </VeltProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Integrate Velt User Identification in the Header

In this section, we'll enhance the Header component by integrating Velt's user identification system. This will sync user data with Velt for collaboration features. We'll update imports and add logic to identify users.

  1. Adding Velt Imports Update the imports in your Header component to include Velt's hooks and components. Add the following lines to the existing imports:

    import { useVeltClient, VeltPresence } from "@veltdev/react";
    
  2. Accessing the Velt Client

    Inside the Header component, use the useVeltClient hook to get the Velt client instance:

    This allows us to interact with Velt's API to identify users.

    const { client } = useVeltClient();
    
  3. Identifying Users with Velt

    Add a useEffect hook to sync the current user from your user-store with Velt's system. This ensures Velt recognizes the active user for future collaboration features. Insert this code after the existing useEffect for user login:

    useEffect(() => {
      if (!client || !user) return;
      const veltUser = {
        userId: user.uid,
        organizationId: user.organizationId,
        name: user.displayName,
        email: user.email,
        photoUrl: user.photoURL,
        color: user.colorCode,
        textColor: user.textColor,
      };
      client.identify(veltUser);
    }, [client, user]);
    

Final: Add Velt Comments to the Spreadsheet

In this section, we'll enhance the SpreadsheetPage component by integrating Velt's commenting system. This allows users to add comments to specific cells and the document as a whole, enhancing collaboration. We'll explain the new additions step-by-step.

import {
  VeltComments,
  VeltCommentTool,
  VeltCommentBubble,
  useSetDocument,
} from "@veltdev/react";
Enter fullscreen mode Exit fullscreen mode
  • VeltComments: The main commenting component that manages the comment UI (e.g., sidebar or popover).VeltCommentTool: A toolbar button for initiating comments.VeltCommentBubble: A UI element that appears near a cell to show or add comments.useSetDocument: A hook to set the current document context for Velt.

  • VeltComments: The main commenting component that manages the comment UI (e.g., sidebar or popover).

  • VeltCommentTool: A toolbar button for initiating comments.

  • useSetDocument: A hook to set the current document context for Velt.

Update SpreadsheetPage Logic

In this section we add some routing logic to access the id from the URL and register the current spreadsheet with Velt using its id and filename. This ensures comments are scoped to the correct document

useSetDocument(id, { documentName: filename });
Enter fullscreen mode Exit fullscreen mode

Add Commenting UI to the JSX

Now, we integrate Velt’s commenting components into the spreadsheet layout:

  1. Document-Wide Comments

    Add it to the top of the JSX, to enable a popover-style comment interface for the entire document.

    <VeltComments popoverMode={true} />
    
  2. Comment Tool

    Add this below the Header , this provides document -level comments, styled to align right in a toolbar-like section.

    <div className="flex justify-end px-4 py-1 bg-gray-50 border-b border-gray-200">
      <VeltCommentTool />
    </div>
    
  3. Single Comment Tool: In the table JSX above, we should add a unique id for every cell in our table data-velt-target-comment-element-id={*cell*.id} as an attribute on each element you want to add comments on. Now, when you click on the Comment Tool and click on the target element, it will attach a Popover comment to the elements.

    Edit the section where we have a single cell and include the id like this:

    {row.getVisibleCells().map((cell) => (
      <div
        key={cell.id}
        data-velt-target-comment-element-id={cell.id}
        style={{ width: `${cell.column.getSize()}px` }}
        className="border-r border-gray-200 h-6 overflow-hidden"
      >
        {flexRender(
          cell.column.columnDef.cell,
          cell.getContext()
        )}
      </div>
    ))}
    

After this step, your app is now ready to be used with full functionality.

thank you gif

Demo

Check the app we have built so far in action:

Next Steps and Conclusion

With Velt integrated, your sheets app is now fully collaborative with real-time features. Each workspace is tied to a unique organization, supporting multiple users and documents. Teammates can comment directly on specific cells, just like in Google Sheets.

Enhance this App with more Velt features:

Velt goes beyond basic commenting. You will also get:

  • In-App Notifications: Use <VeltNotifications /> to alert users about cell updates, new comments, or document changes.

  • Recorder: Add <VeltRecorder /> for users to attach audio, video, or screen recordings to comments.

  • Reactions: Implement <VeltReactions /> on cells or comments for quick feedback.

  • Presence & Cursors: Reintroduce <VeltPresence /> and <VeltCursors /> to show active users and their locations in the spreadsheet.

  • Live State Sync: Keep cell data synchronized across users in real time.

  • View Analytics: Track spreadsheet views with <VeltViewAnalytics />.

You can explore all of Velt’s features here

If your app already has a commenting system, Velt provides backend APIs to seamlessly migrate existing comments. This ensures a smooth transition without losing historical data. The integration process is flexible, allowing you to sync past comments with Velt’s real-time infrastructure while keeping your authentication system intact. Try Velt SDK and build a more scalable apps with advance collaborative features!

Thank you for reading🙏!


Resources

For more advanced customization, follow the official Velt UI guide:

👉 UI Customization Overview

đź”— Explore the complete source code on GitHub

Or try the live demo and test the real-time commenting across multiple tabs


If you found this article useful, share it with your peers and community.

If You ❤️ My Content! Connect Me on Twitter

Check SaaS Tools I Use 👉🏼Access here!

I am open to collaborating on Blog Articles and Guest Posts🫱🏼‍🫲🏼 📅Contact Here

Heroku

Amplify your impact where it matters most — building exceptional apps.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (6)

Collapse
 
arindam_1729 profile image
Arindam Majumder •

Great Write-up

Collapse
 
astrodevil profile image
Astrodevil •

Appreciate your support Arindam!

Collapse
 
nadeem_zia_257af7e986ffc6 profile image
nadeem zia •

Interesting to read

Collapse
 
astrodevil profile image
Astrodevil •

Thankyou

Collapse
 
xtitan profile image
Abhinav •

helpful

Collapse
 
astrodevil profile image
Astrodevil •

thankyou

PulumiUP 2025 image

PulumiUP 2025: Cloud Innovation Starts Here

Get inspired by experts at PulumiUP. Discover the latest in platform engineering, IaC, and DevOps. Keynote, demos, panel, and Q&A with Pulumi engineers.

Register Now

đź‘‹ Kindness is contagious

Engage with a wealth of insights in this thoughtful article, cherished by the supportive DEV Community. Coders of every background are encouraged to bring their perspectives and bolster our collective wisdom.

A sincere “thank you” often brightens someone’s day—share yours in the comments below!

On DEV, the act of sharing knowledge eases our journey and forges stronger community ties. Found value in this? A quick thank-you to the author can make a world of difference.

Okay