Collaboration and Sharing
Enable teamwork through real-time interaction patterns, presence indicators, and seamless content distribution across platforms and devices.
Real-time collaboration demo
See how presence indicators, avatar stacks, and sharing controls work together to create a connected workspace.
JR
AS
SK
3 people active
Design System v2.0
Updated 2 min ago
JR
AS
SK
+1
Q2 Roadmap
Updated 1 hr ago
JR
AS
Common patterns
Real-time editing
Multiple users editing a document simultaneously with live cursor tracking
Comment threads
Contextual discussions anchored to specific elements
Shared workspaces
Central hub for team resources and activity feeds
Permission levels
Granular access control from view-only to admin
Presence indicator anatomy
Usage guidelines
✓ Do
- ·Show live presence for collaborative documents
- ·Provide multiple sharing options (link, email, copy)
- ·Use avatar stacks to show collaborators efficiently
- ·Offer granular permission controls
✗ Don't
- ·Don't show full names in crowded avatar stacks
- ·Avoid auto-sharing without explicit consent
- ·Don't expose sensitive data in share previews
- ·Avoid overwhelming users with too many collaboration options
Implementation
CollaborationFeatures.tsx
tsx
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Copy, Check, Users, Link as LinkIcon, Mail, MessageCircle } from "lucide-react";
import { cn } from "@/lib";
// ── Share component ────────────────────────────────────────
type SharePlatform = "email" | "link" | "message" | "copy";
interface ShareProps {
title: string;
url: string;
text?: string;
platforms?: SharePlatform[];
}
export function Share({ title, url, text, platforms = ["link", "email", "message", "copy"] }: ShareProps) {
const [copied, setCopied] = useState(false);
const shareUrl = `${window.location.origin}${url}`;
async function handleShare(platform: SharePlatform) {
switch (platform) {
case "copy":
await navigator.clipboard.writeText(shareUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
break;
case "email":
window.location.href = `mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(text || "")} ${encodeURIComponent(shareUrl)}`;
break;
case "message":
if (navigator.share) {
await navigator.share({ title, text, url: shareUrl });
}
break;
case "link":
// Open copy modal or dropdown
break;
}
}
return (
<div className="flex items-center gap-2">
{platforms.map((platform) => {
const icons = {
copy: copied ? Check : Copy,
link: LinkIcon,
email: Mail,
message: MessageCircle,
};
const labels = {
copy: copied ? "Copied!" : "Copy",
link: "Share link",
email: "Email",
message: "Message",
};
const Icon = icons[platform];
return (
<button
key={platform}
onClick={() => handleShare(platform)}
className={cn(
"px-3 py-1.5 rounded-lg text-[12px] font-medium border transition-colors flex items-center gap-1.5",
"bg-[rgb(var(--surface))] border-[rgb(var(--border))] text-[rgb(var(--text-secondary))]",
"hover:border-[rgb(var(--accent))] hover:text-[rgb(var(--text-primary))]"
)}
disabled={copied && platform === "copy"}
>
<Icon className="w-3.5 h-3.5" />
{labels[platform as keyof typeof labels]}
</button>
);
})}
</div>
);
}
// ── Collaborator Avatar stack ─────────────────────────────
interface Collaborator {
id: string;
name: string;
avatar?: string;
color?: string;
}
interface CollaboratorAvatarsProps {
collaborators: Collaborator[];
maxDisplay?: number;
size?: "sm" | "md" | "lg";
}
export function CollaboratorAvatars({ collaborators, maxDisplay = 3, size = "md" }: CollaboratorAvatarsProps) {
const sizeClasses = {
sm: "w-6 h-6 text-[10px]",
md: "w-8 h-8 text-[12px]",
lg: "w-10 h-10 text-[14px]",
};
const offsetSize = {
sm: "-space-x-1",
md: "-space-x-2",
lg: "-space-x-3",
};
const display = collaborators.slice(0, maxDisplay);
const remaining = collaborators.length - maxDisplay;
return (
<div className={cn("flex", offsetSize[size])}>
{display.map((c, i) => (
<div
key={c.id}
className={cn(
"relative rounded-full border-2 border-[rgb(var(--surface))] flex items-center justify-center font-medium text-white",
sizeClasses[size]
)}
style={{
background: c.color || `hsl(${(i * 137) % 360}, 70%, 50%)`,
zIndex: display.length - i,
}}
title={c.name}
>
{c.avatar ? (
<img src={c.avatar} alt={c.name} className="w-full h-full rounded-full object-cover" />
) : (
c.name.split(" ").map((n) => n[0]).join("").slice(0, 2)
)}
</div>
))}
{remaining > 0 && (
<div
className={cn(
"relative rounded-full border-2 border-[rgb(var(--surface))] bg-[rgb(var(--surface-raised))] flex items-center justify-center font-medium text-[rgb(var(--text-secondary))]",
sizeClasses[size]
)}
style={{ zIndex: 0 }}
>
+{remaining}
</div>
)}
</div>
);
}
// ── Presence indicator ────────────────────────────────────
interface PresenceIndicatorProps {
activeUsers: {
id: string;
name: string;
color?: string;
cursor?: { x: number; y: number };
}[];
}
export function PresenceIndicator({ activeUsers }: PresenceIndicatorProps) {
return (
<div className="relative">
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full bg-[#22c55e] animate-pulse" />
<span className="text-[11px] text-[rgb(var(--text-tertiary))]">
{activeUsers.length} {activeUsers.length === 1 ? "person" : "people"} active
</span>
</div>
{activeUsers.map((user) => (
<div
key={user.id}
className="absolute pointer-events-none transition-all"
style={{
left: user.cursor?.x ?? 0,
top: user.cursor?.y ?? 0,
}}
>
<div className="flex items-center gap-1">
<div
className="w-3 h-3 rounded-full border-2 border-white"
style={{ background: user.color || "#3b82f6" }}
/>
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-white text-black whitespace-nowrap">
{user.name}
</span>
</div>
</div>
))}
</div>
);
}
// ── Collaboration demo ────────────────────────────────────
interface Document {
id: string;
title: string;
collaborators: Collaborator[];
updatedAt: string;
}
export function CollaborationDemo() {
const [activeUsers] = useState([
{ id: "1", name: "Jamie", color: "#22c55e" },
{ id: "2", name: "Alex", color: "#3b82f6" },
{ id: "3", name: "Sam", color: "#f59e0b" },
]);
const [documents] = useState<Document[]>([
{
id: "1",
title: "Design System v2.0",
collaborators: [
{ id: "1", name: "Jamie", color: "#22c55e" },
{ id: "2", name: "Alex", color: "#3b82f6" },
{ id: "3", name: "Sam", color: "#f59e0b" },
{ id: "4", name: "Taylor", color: "#ec4899" },
],
updatedAt: "2 minutes ago",
},
{
id: "2",
title: "Q2 Roadmap",
collaborators: [
{ id: "1", name: "Jamie", color: "#22c55e" },
{ id: "2", name: "Alex", color: "#3b82f6" },
],
updatedAt: "1 hour ago",
},
]);
return (
<div className="space-y-6">
{/* Header with presence */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-[14px] font-semibold text-[rgb(var(--text-primary))]">Active collaborators</h3>
<PresenceIndicator activeUsers={activeUsers} />
</div>
<Share title="Design System" url="/design-system" text="Check out our latest design system updates" />
</div>
{/* Document list */}
<div className="space-y-3">
{documents.map((doc) => (
<div
key={doc.id}
className="p-4 rounded-xl border border-[rgb(var(--border))] bg-[rgb(var(--surface))] hover:border-[rgb(var(--accent))] transition-colors"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h4 className="text-[13px] font-medium text-[rgb(var(--text-primary))]">{doc.title}</h4>
<p className="text-[11px] text-[rgb(var(--text-tertiary))] mt-1">Updated {doc.updatedAt}</p>
</div>
<CollaboratorAvatars collaborators={doc.collaborators} size="sm" />
</div>
</div>
))}
</div>
</div>
);
}Accessibility
- →Share buttons must have descriptive aria-labels for screen readers.
- →Presence indicators should announce changes for assistive technology.
- →Collaborator avatars need alt text with names.
- →Real-time content changes should have polite aria-live announcements.
- →Provide keyboard shortcuts for common collaboration actions.
- →Cursor positions should be announced when they move to new regions.