DEV Community

Cover image for Building a Collaborative Trello-Style Kanban Board with Next.js, Velt and v0🔥
Astrodevil
Astrodevil

Posted on

Building a Collaborative Trello-Style Kanban Board with Next.js, Velt and v0🔥

Over the years, Trello has matured into a feature-rich project management product, complete with real-time features and power-ups. Yet, great as a standalone product, it can be a constraint.

What if you need a lightweight, focused Kanban board that you can integrate into your company's internal dashboard, a client portal, or a specialized production tool? Using an iframe or a separate tab for Trello will create a disjointed user experience. Context is broken, and collaboration is kept away from the primary working environment.

This tutorial guides you on how to build a collaborative Trello-clone Kanban board. You'll use Next.js as the full-stack framework, Velt SDK for injecting real-time collaboration features like presence, contextual comments, and notifications, and Vercel v0 for rapid UI prototyping. The result of this tutorial is not just another task manager, but an integrated collaborative Kanban board that you can adapt and extend for any use case. This means you can put the functionality of Trello wherever your product needs it.

What is Velt?

Velt is a developer platform that enables developers to integrate AI-powered, real-time collaboration into their web apps. Velt's SDK provides pre-built components for features like comments and live presence, saving you from the complexity of building and syncing a real-time backend yourself.

Using Velt reduces development time, allowing you to focus on your app's unique features.

Generating the UI of the Trello-clone app

Building out the Kanban board UI manually will take days. That’s why we’ll use v0 to generate the UI code, speeding up our development process.

A short note on v0.

v0 is an AI-powered UI and code generator that lets developers build UI components and pages using natural language prompts. v0 speeds up development by generating UI pages and components, allowing you to focus on integrations and app functionality.

To use v0, go to the v0 official website.

Use the following prompt to generate the UI and basic functionality of the Kanban board:

You are designing a Trello-style board interface with a clean, minimal, yet powerful look and feel. Don’t implement collaboration logic—just build the UI scaffolding so it’s ready to integrate features via the Velt SDK later.

## UI Requirements

### 1. Layout & Navigation
-   A responsive top navbar that includes:
    -   User switcher dropdown: shows current user’s avatar/name; dropdown lists available user identities.
    -   Dark / Light mode toggle with smooth transition.
    -   Board title and optional “Add List” button.

### 2. Board & Lists
-   Boards contain vertical lists (columns).
-   Each list includes:
    -   Title (editable).
    -   Options menu (⋯).
    -   “Add Card” button.

### 3. Cards
-   Cards display:
    -   Title text.
    -   Optional avatar icons for the assigned users.
    -   Reactions area underneath card title (just UI placeholders like 👍, ❤️, 🎉).
    -   “Comment count” badge placeholder.
-   Cards are draggable between lists (UI only; no drag logic).

### 4. Comments & Reactions
-   Inline comment UI on the card detail pane:
    -   Text area for “Add a comment…”
    -   List of comments, each showing user avatar/name, timestamp, and text.
    -   Reactions toolbar next to each comment (UI only).
-   On card previews: show comment count and reaction count badges.

### 5. Real-Time Presence Indicators
-   In the navbar or top-right: placeholder UI showing colored dot avatars representing “currently online” users (use mock images).

### 6. Theme Support
-   Full Light and Dark mode styling.
-   Include a toggle switch that seamlessly swaps variables/colors.

### 7. Minimal & Effective Design
-   Use clean typography, intuitive spacing.
-   Components should feel lightweight and polished.
-   Avoid visual clutter—focus on clarity and usability.

## Integration-Ready Features
-   Use data attributes or placeholder callbacks (e.g., `onCommentClick`, `onToggleReaction`, `onUserSwitch`) so later you can hook up Velt SDK features:
    -   Commenting (Figma-style or popover)—Velt supports this out of the box :contentReference[oaicite:1]{index=1}.
    -   Reactions engine placeholders.
    -   User presence cursors, user switcher.
-   Ensure comments & reactions have identifiable DOM structures or `data-velt-target-*` attributes ready for Velt injection :contentReference[oaicite:2]{index=2}.
-   Provide empty container elements (e.g. `<div id="velt-comment-root"></div>`) where Velt can mount.
-   Mock data placeholders (cards, comments, users) are fine—logic to fetch and mutate will come later.

## Tech & Style
-   Build using your preferred UI framework (React, Vue, Svelte, etc.).
-   Use CSS variables or theming solution for dark/light mode.
-   Avoid heavy UI libraries—keep it minimal and easy to customize.

---

**Goal:** Deliver a UI skeleton for a collaborative task board that looks and behaves beautifully, with clear spots marked to plug in Velt-powered collaboration later.
Enter fullscreen mode Exit fullscreen mode

Now you’ve generated the UI code, the next step is to set up a Next.js project to house the app.

Final app will look like this:

Velt-Trello Clone

Setting Up the Project

In this section, you’ll set up your Next.js app and get your Velt API key.

Scaffolding a new Next.js project

Use the following command to scaffold a new project using the default configuration:

npx create-next-app@latest --yes
Enter fullscreen mode Exit fullscreen mode

The --yes flag skips the prompts using saved preferences or defaults. The default setup enables TypeScript, TailwindCSS, App Router, Turbopack, with import alias @/*. This default configuration is perfect for our tutorial.

For this tutorial, you’ll need to install the following packages:

  • veltdev/react and veltdev/types: for real-time collaboration features.
  • Shadcn: for UI components.
  • Zod: for input validation.
  • @dnd-kit/core: for drag-and-drop functionality.

Use the following command to install the necessary packages:

npm install veltdev/react veltdev/types zod @dnd-kit/core
Enter fullscreen mode Exit fullscreen mode

Use the following command to initialize Shadcn:

npx shadcn@latest init
Enter fullscreen mode Exit fullscreen mode

Getting your Velt API key

To obtain your API key, go to the Velt console.

Add the API key to your .env file.

NEXT_PUBLIC_VELT_API_KEY=your-api-key
Enter fullscreen mode Exit fullscreen mode

Your project is set up. The next section focuses on adding real-time collaborative features to your app.

Adding Real-Time Collaboration with Velt

In this section, you’ll add real-time collaborative features to your Kanban board using Velt. The Velt collaborative features you’ll be adding are:

  • Comments
  • Live State Sync
  • Presence
  • Notifications

Wrapping your app in the VeltProvider component

In your layout.tsx file, wrap your app with the VeltProvider component.

 <VeltProvider apiKey={process.env.NEXT_PUBLIC_API_KEY}>
   {children}
 </VeltProvider>
Enter fullscreen mode Exit fullscreen mode

The React Provider pattern allows child components to access parent props without prop drilling. By wrapping your app in the VeltProvider, any Velt component can access the Velt client globally.

Implementing user authentication

User authentication is crucial when adding any Velt collaboration feature to your app. Velt needs to know your app users to attribute comments and show who is online.

In your application, user authentication will be part of your authentication system. However, this tutorial uses a mock user database and user switcher to simulate real user authentication.

Creating user management utility functions

Create a user-manager.ts file in your lib folder, and add the following code:

const HARDCODED_USERS = [
  {
    userId: 'user_alice_johnson',
    name: 'Alice Johnson',
    email: 'alice@example.com',
    organizationId: 'trello-board-org',
    photoUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=user_alice_johnson'
  },
  {
    userId: 'user_bob_smith',
    name: 'Bob Smith',
    email: 'bob@example.com',
    organizationId: 'trello-board-org',
    photoUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=user_bob_smith'
  }
]

const getCurrentUserIndex = (): number => {
  if (typeof window === 'undefined') return 0
  const storedIndex = localStorage.getItem('velt_current_user_index')
  return storedIndex ? parseInt(storedIndex, 10) : 0
}

const setCurrentUserIndex = (index: number): void => {
  if (typeof window === 'undefined') return
  localStorage.setItem('velt_current_user_index', index.toString())
}

export const getCurrentUser = () => {
  if (typeof window === 'undefined') return null

  const currentIndex = getCurrentUserIndex()
  return HARDCODED_USERS[currentIndex]
}

export const getOrCreateUser = () => {
  return getCurrentUser()
}

export const switchUser = () => {
  if (typeof window === 'undefined') return null

  const currentIndex = getCurrentUserIndex()
  const newIndex = currentIndex === 0 ? 1 : 0
  setCurrentUserIndex(newIndex)

  return HARDCODED_USERS[newIndex]
}

export const getAvailableUsers = () => {
  return HARDCODED_USERS
}

export const getUserById = (userId: string) => {
  return HARDCODED_USERS.find(user => user.userId === userId)
}
Enter fullscreen mode Exit fullscreen mode

Let’s take a look at what is happening in this code:

  • HARDCODED_USERS stores the mock users.
  • getCurrentUserIndex maintains the user session across browser refreshes by retrieving the last active user from localStorage.
  • setCurrentUserIndex persists the current user’s session in the browser’s localStorage.
  • getCurrentUser returns the complete user object for the authenticated user (in this case, the selected user).
  • getOrCreateUser fetches or initializes a user.
  • The switchUser() function allows for toggling between different users. This simulates logging in and logging out users.
  • getAvailableUsers return all available users.
  • getUserById returns a user from their ID.

Creating the auth component

The auth component handles Velt identification and the user context of the app.

For this tutorial, the focus will be on the relevant parts of the auth component. Check the repo in the “Resources” section for the full code.

useEffect(() => {
  if (!client) return;
  const initializeUser = async () => {
    // ...prevent double init...
    const user = getOrCreateUser();
    await client.identify(user, { forceReset: userSwitchTrigger > 0 });
    await client.setDocument("trello-board", {
      documentName: "Product Development Board",
    });
    // ...set user contacts...
    setIsInitialized(true);
  };
  setIsInitialized(false);
  initializeUser();
}, [client, userSwitchTrigger]);
Enter fullscreen mode Exit fullscreen mode

When the component mounts or the current user changes:

  • The Velt client is initialized.
  • The current user is identified to Velt using client.identify().
  • A collaborative document is set using client.setDocument(). The setDocument method on the Velt client takes in two parameters: documentId and optional metadata. A document is a shared collaborative space where users can interact.
const availableUsers = getAvailableUsers();
const userContacts = availableUsers.map((u) => ({
  userId: u.userId,
  name: u.name,
  email: u.email,
  photoUrl: u.photoUrl,
}));
await client.setUserContacts(userContacts);
Enter fullscreen mode Exit fullscreen mode

In this part of the code, you’re setting up available users for mentions (when a user types the “@” symbol) in the Velt comments.

useEffect(() => {
  const handleUserSwitch = () => {
    setUserSwitchTrigger((prev) => prev + 1);
  };
  window.addEventListener("velt-user-switch", handleUserSwitch);
  return () =>
    window.removeEventListener("velt-user-switch", handleUserSwitch);
}, []);
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, the component listens for a custom event to trigger user switching and re-initialization. This simulates logging in and logging out a user.

The component renders nothing until the Velt is initialized.

if (!isInitialized) {
  return null;
}
return null;
Enter fullscreen mode Exit fullscreen mode

Now that you’re done with creating your component, add it to your app’s entry point, usually the page.tsx file.

Note: the auth component should be called in a child component, never in the same file as your VeltProvider.

In the next subsections, you’ll add the collaborative features to your app.

Adding Live Presence with VeltPresence

Now that users can authenticate, let’s make them visible to each other in real-time.

Presence lets users know who’s online and working on what, which reduces conflicts. The VeltPresence component will be wrapped in another component, DynamicVeltPresence.

Create a velt-presence-dynamic.tsx file and add the following code:

"use client"

import dynamic from 'next/dynamic'
import { useTheme } from '@/components/theme-provider'

const VeltPresence = dynamic(
  () => import('@veltdev/react').then(mod => ({ default: mod.VeltPresence })),
  {
    ssr: false,
    loading: () => <div className="h-6 w-16 bg-muted animate-pulse rounded-full" />
  }
)

function VeltPresenceWrapper() {
  const { resolvedTheme } = useTheme()
  return (
    <div className="flex items-center justify-center">
      <VeltPresence darkMode={resolvedTheme === 'dark'} />
    </div>
  )
}

export { VeltPresenceWrapper as DynamicVeltPresence }
Enter fullscreen mode Exit fullscreen mode

By using the dynamic import, VeltPresence only loads when it is required. ssr: false tells Next.js that the component should not be rendered on the server.

The VeltPresence component is wrapped in VeltPresenceWrapper, which is exported as DynamicVeltPresence.

The nav bar component renders the DynamicVeltPresence component.

Now that users know who’s online, you’ll make their actions visible in real-time through live data synchronization.

Integrating Live Data Sync

The magic of collaboration happens when changes appear instantly across all connected users. You’ll achieve that using Velt’s Live State Sync feature.

Create a custom hook file, use-live-board-sync.ts. This file is the core of Live State Sync in the app, enabling real-time board data sharing and updates across all users.

This tutorial only focuses on the important parts of the file. Check the repo for the full code.

const syncedBoardData = useLiveStateData(BOARD_SYNC_ID, {
  listenToNewChangesOnly: false
})

useSetLiveStateData(BOARD_SYNC_ID, localBoardData, { merge: false })
Enter fullscreen mode Exit fullscreen mode

useLiveStateData hook from Velt listens for changes to the board data from all connected clients. The hook accepts two parameters:

  • liveStateDataId: a unique string ID to identify the live data to retrieve.
  • liveStateDataConfig(optional): configuration object for controlling data retrieval behaviour. listenToNewChangesOnly property specifies whether to receive only new changes from the client subscribed.

The useSetLiveStateData hook, from Velt, pushes changes to all connected clients. The hook accepts three parameters:

  • liveStateDataId (required): A unique string ID to identify the data.
  • liveStateData (required): The data to sync. It can be objects, arrays, strings, or numbers.
  • setLiveStateDataConfig (optional): Configuration object. merge specifies whether to merge the data with existing data (default: false).
useEffect(() => {
  if (!isInitialized && syncedBoardData && Object.keys(syncedBoardData).length > 0) {
    setLocalBoardData(syncedBoardData as BoardData)
    setIsInitialized(true)
  } else if (!isInitialized) {
    setIsInitialized(true)
  }
}, [syncedBoardData, isInitialized])
Enter fullscreen mode Exit fullscreen mode

On first load, the hook checks if there’s already synced data. If so, it uses that; otherwise, it falls back to local initial data.

useEffect(() => {
  if (isInitialized && syncedBoardData && Object.keys(syncedBoardData).length > 0) {
    const newData = syncedBoardData as BoardData
    if (JSON.stringify(newData) !== JSON.stringify(localBoardData)) {
      setLocalBoardData(newData)
    }
  }
}, [syncedBoardData, isInitialized, localBoardData])
Enter fullscreen mode Exit fullscreen mode

Whenever the synced data changes (e.g., another user adds a card), the local state updates to reflect those changes.

const addCard = useCallback((listId: string, title: string, currentUser: any) => {
  // ...create newCard...
  updateBoardData(prev => ({
    ...prev,
    lists: prev.lists.map(list => 
      list.id === listId 
        ? { ...list, cards: [...list.cards, newCard] }
        : list
    )
  }))
}, [updateBoardData])
Enter fullscreen mode Exit fullscreen mode

All board operations (add card, delete card, move card, add list) update the local state, which is then broadcast to all clients via Velt.

Now that users can see each other’s actions, the next step is to add contextual communication through commenting.

Adding Comments in cards

The Comments feature allows users to add comments in the Kanban board cards.

You’ll use the VeltComments and VeltCommentsSidebar components, from Velt, to add live commenting to your app. These components provide a plug-and-play way to enable collaborative comments in your app.

Similar to Presence, you’ll dynamically import these components so that they are only rendered when needed.

// velt-comments-dynamic.tsx

"use client";

import dynamic from "next/dynamic";
import { useTheme } from "@/components/theme-provider";

const VeltComments = dynamic(
  () => import("@veltdev/react").then((mod) => ({ default: mod.VeltComments })),
  {
    ssr: false,
    loading: () => null,
  }
);

const VeltCommentsSidebar = dynamic(
  () =>
    import("@veltdev/react").then((mod) => ({
      default: mod.VeltCommentsSidebar,
    })),
  {
    ssr: false,
    loading: () => null,
  }
);

const VeltSidebarButton = dynamic(
  () =>
    import("@veltdev/react").then((mod) => ({
      default: mod.VeltSidebarButton,
    })),
  {
    ssr: false,
    loading: () => null,
  }
);

function VeltCommentsWrapper({
  popoverMode = false,
}: {
  popoverMode?: boolean;
}) {
  const { resolvedTheme } = useTheme();
  return (
    <VeltComments
      popoverMode={popoverMode}
      darkMode={resolvedTheme === "dark"}
    />
  );
}

function VeltCommentsSidebarWrapper() {
  const { resolvedTheme } = useTheme();
  return <VeltCommentsSidebar darkMode={resolvedTheme === "dark"} />;
}

function VeltSidebarButtonWrapper() {
  const { resolvedTheme } = useTheme();
  return <VeltSidebarButton darkMode={resolvedTheme === "dark"} />;
}

export {
  VeltCommentsWrapper as DynamicVeltComments,
  VeltCommentsSidebarWrapper as DynamicVeltCommentsSidebar,
  VeltSidebarButtonWrapper as DynamicVeltSidebarButton,
};

Enter fullscreen mode Exit fullscreen mode

Render the components, DynamicVeltComments and DynamicVeltCommentsSidebar in the home page, where your Kanban board is rendered.

export default function Home() {
  // other parts of the code
  return (
    <ThemeProvider>
      <div className="min-h-screen bg-background">
        {/* rest of the code */}

        {/* Velt Comments Components */}
        <DynamicVeltComments popoverMode={true} />
        <DynamicVeltCommentsSidebar />
      </div>
    </ThemeProvider>
  );
}

Enter fullscreen mode Exit fullscreen mode

Render the DynamicVeltSidebarButton component in the nav bar.

export function Navbar({ currentUser, onUserSwitch, boardTitle }: NavbarProps) {
  // Parts of the Navbar code omitted for brevity
  return (
    <nav className="border-b bg-card/50 backdrop-blur supports-[backdrop-filter]:bg-card/50">
      <div className="flex h-14 sm:h-16 items-center justify-between px-3 sm:px-6">
        {/* Other Navbar Items */}
        <div className="flex items-center gap-4">
          {/* Comments Sidebar */}
          <div className="hidden sm:flex items-center justify-center h-9 w-9">
            <DynamicVeltSidebarButton />
          </div>

          {/* Rest of the Navbar */}
        </div>
      </div>
    </nav>
  );
}

Enter fullscreen mode Exit fullscreen mode

Now that users can leave comments, they need to know when other users add those comments. In the next subsection, you’ll add real-time notification to your app.

Adding real-time notification

With real-time notifications, the concerned user is notified when they’re tagged in a comment or mentioned.

To enable notifications, go to the Notifications section in the Configurations section of the Velt Console and toggle the enable button.

Velt console

Similar to adding comments, the VeltNotificationTool component will be dynamically imported, so it only renders when needed.

// velt-notifications-dynamic.tsx

"use client"

import dynamic from 'next/dynamic'
import { useTheme } from '@/components/theme-provider'

const VeltNotificationsTool = dynamic(
  () => import('@veltdev/react').then(mod => ({ default: mod.VeltNotificationsTool })),
  {
    ssr: false,
    loading: () => null
  }
)

function VeltNotificationsToolWrapper() {
  const { theme } = useTheme()
  return <VeltNotificationsTool darkMode={theme === 'dark'} />
}

export { VeltNotificationsToolWrapper as DynamicVeltNotificationsTool }
Enter fullscreen mode Exit fullscreen mode

Import and use the DynamicVeltNotificationsTool in your nav bar.

"use client";
import { DynamicVeltNotificationsTool } from "./velt-notifications-dynamic";

interface NavbarProps {
  currentUser: any;
  onUserSwitch: () => void;
  boardTitle: string;
}

export function Navbar({ currentUser, onUserSwitch, boardTitle }: NavbarProps) {
  // other parts of the code remain unchanged
  return (
    <nav className="border-b bg-card/50 backdrop-blur supports-[backdrop-filter]:bg-card/50">
      <div className="flex h-14 sm:h-16 items-center justify-between px-3 sm:px-6">
        {/* Other elements */}
        {/* Right section - Presence, Theme, User */}
        <div className="flex items-center gap-4">
          {/* Notifications */}
          <div className="hidden sm:flex items-center justify-center h-9 w-9">
            <DynamicVeltNotificationsTool />
          </div>
          {/* rest of the code */}
        </div>
      </div>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

You’ve added collaborative features to your Kanban board app.

**Note:* By adding these collaboration features, you enable a range of collaboration features by default. These features include read receipts, emoji reactions, status updates, and threaded replies, which come without writing extra lines of code.*

In the next section, you’ll understand, on a high level, how the drag-and-drop functionality and the Kanban board work.

Enabling Kanban Board Interactions

In this section, you’ll learn how the drag-and-drop functionality and the Kanban board work on a high level.

The drag-and-drop functionality uses the @dnd-kit/core library for smooth interactions. However, the real magic happens in how these movements sync across all connected users:

//page.tsx

// Set up drag sensors with activation distance to prevent accidental drags
const sensors = useSensors(
  useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
)

// Handle drag operations
const handleDragStart = (event: DragStartEvent) => {
  const { active } = event
  const card = boardData.lists.flatMap((list) => list.cards)
    .find((card) => card.id === active.id)
  setActiveCard(card || null) // Show drag preview
}

const handleDragEnd = (event: DragEndEvent) => {
  const { active, over } = event
  if (!over) return

  const activeCardId = active.id as string
  const overListId = over.id as string

  // This single function call updates ALL connected users instantly
  moveCard(activeCardId, overListId)
}
Enter fullscreen mode Exit fullscreen mode

The drag system wraps the entire board, making every card draggable and every list a drop target:

// page.tsx

<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
  <Board
    board={boardData}
    onCardClick={handleCardClick}
    onAddCard={handleAddCard}
    onDeleteCard={handleDeleteCard}
  />
  <DragOverlay>
    {activeCard && (
      <Card className="cursor-grabbing shadow-lg opacity-50">
        <CardContent className="p-3">
          <h4 className="text-sm font-medium">{activeCard.title}</h4>
        </CardContent>
      </Card>
    )}
  </DragOverlay>
</DndContext>
Enter fullscreen mode Exit fullscreen mode

Syncing card state in real-time

This is where Velt transforms a simple drag-and-drop system into a collaborative experience. The moveCard function shows the power of Velt's live state synchronization:

const moveCard = useCallback((cardId: string, targetListId: string) => {
  updateBoardData(prev => {
    let sourceCard: BoardCard | null = null

    // Remove card from current list
    const listsWithoutCard = prev.lists.map(list => {
      const cardIndex = list.cards.findIndex(card => card.id === cardId)
      if (cardIndex !== -1) {
        sourceCard = list.cards[cardIndex]
        return {
          ...list,
          cards: list.cards.filter((_, index) => index !== cardIndex)
        }
      }
      return list
    })

    // Add card to target list
    if (sourceCard) {
      return {
        ...prev,
        lists: listsWithoutCard.map(list => 
          list.id === targetListId 
            ? { ...list, cards: [...list.cards, sourceCard!] }
            : list
        )
      }
    }
    return prev
  })
}, [updateBoardData])
Enter fullscreen mode Exit fullscreen mode

The updateBoardData function is at the core of the movecard function. The updateBoardData function updates local state for immediate UI response. Velt automatically syncs the changes to all connected users:

// This hook handles the bi-directional sync magic
const syncedBoardData = useLiveStateData(BOARD_SYNC_ID, {
  listenToNewChangesOnly: false
})

useSetLiveStateData(BOARD_SYNC_ID, localBoardData, { merge: false })
Enter fullscreen mode Exit fullscreen mode

When Alice drags a card from "To Do" to "In Progress", Bob sees the movement happen instantly on his screen; no refresh required.

Attaching comments to individual cards

With Velt’s Comment feature, every card becomes a collaborative workspace; thanks to the data-velt-target attribute.

function DraggableCard({ card, onCardClick }) {
  return (
    <Card
      className="cursor-pointer hover:shadow-md transition-shadow"
      onClick={() => onCardClick(card.id)}
      data-velt-target={`card-${card.id}`} // This makes comments contextual
      {...listeners}
      {...attributes}
    >
      <CardContent className="p-3">
        <h4 className="text-sm font-medium mb-2">{card.title}</h4>

        {/* Card content with reactions */}
        {card.reactions.length > 0 && (
          <div className="flex flex-wrap gap-1 mb-3" data-velt-reactions={`card-${card.id}`}>
            {card.reactions.map((reaction, index) => (
              <Badge key={index} variant="secondary" className="cursor-pointer">
                {reaction.emoji} {reaction.count}
              </Badge>
            ))}
          </div>
        )}
      </CardContent>
    </Card>
  )
}
Enter fullscreen mode Exit fullscreen mode

With Velt's comment components active in your app, users can:

  • Click any card to add contextual comments.
  • See comment indicators on cards with discussions.
  • Access all comments through the sidebar for an overview.

Showing user presence per card/column for multiplayer awareness

Velt's presence system automatically tracks user activity across your board elements. By adding data-velt-target attribute to both the lists and cards, you create awareness zones:

// Lists show presence when users are active in that column
<div className="flex-shrink-0 w-72" data-velt-target={`list-${list.id}`}>
  <Card className="bg-muted/50">
    <CardContent className="p-4">
      <h3 
        className="font-semibold cursor-pointer hover:bg-accent rounded px-2 py-1"
        data-velt-target={`list-title-${list.id}`}
      >
        {list.title}
      </h3>

      {/* Cards inherit presence context */}
      {list.cards.map((card) => (
        <DraggableCard 
          key={card.id} 
          card={card}
          data-velt-target={`card-${card.id}`}
        />
      ))}
    </CardContent>
  </Card>
</div>
Enter fullscreen mode Exit fullscreen mode

The navbar's Presence component shows who's currently active on the board:

<div className="flex items-center justify-center h-9">
  <DynamicVeltPresence />
</div>
Enter fullscreen mode Exit fullscreen mode

This creates natural awareness. Team members can see who's working on which parts of the board, reducing conflicts and enabling better coordination.

The Collaborative Workflow in Action

Here's how these features work together in practice:

  1. Alice starts dragging a card. Bob sees the drag preview in real-time.
  2. Alice drops the card in "In Progress"; the change syncs instantly to Bob's board.
  3. Bob clicks the moved card and adds a comment: "Great progress! Need help with testing?"
  4. Alice gets a notification about Bob's comment and can respond immediately.
  5. Both users see each other's presence indicators, knowing they're collaborating actively.

That's it, you can try the demo app here

Conclusion

That's how you build a collaborative Trello-style Kanban board. In this article, you've learned how to use Velt to add real-time collaboration features to your Kanban board.

The key takeaway is that you don't need a complex backend to add real-time features to your app. Tools like Velt let you focus on your unique app features rather than reinventing the wheel for collaboration.

Velt SDK also has other powerful collaboration features, like:

  • Multiplayer Text Editing: You can integrate Velt's CRDT-powered live editing, built on Yjs, for true Google Docs-style collaboration. This allows for conflict-free, real-time text synchronization, even supporting offline edits.
  • Cursors: allow users to see each other’s cursors when interacting on the same document, making your app feel more alive.

Explore these features to take your collaborative app to the next level.

Resources

Top comments (0)