Skip to main content

Overview

A reactive avatar component that displays the current authenticated user’s image and name. Automatically updates when user data changes through Convex live queries. Shows a loading skeleton while fetching and displays user initials as a fallback.

Features

  • Displays user profile picture
  • Shows user initials as fallback
  • Loading skeleton state
  • Reactive - updates automatically
  • Three size variants (sm, md, lg)
  • Customizable styling
  • TypeScript support
  • Works with Convex Auth

Installation

1

Install the component

npx shadcn@latest add https://convex-ui.vercel.app/r/current-user-avatar-tanstack
2

Prerequisites

This component requires Convex Auth to be configured. Install authentication first:
npx shadcn@latest add https://convex-ui.vercel.app/r/password-based-auth-tanstack
# or
npx shadcn@latest add https://convex-ui.vercel.app/r/social-auth-tanstack
3

Start Convex

npx convex dev

What Gets Installed

Components

  • current-user-avatar.tsx - Main avatar component

Hooks

  • use-current-user-image.ts - Hook to get user’s profile image
  • use-current-user-name.ts - Hook to get user’s name

Backend (Convex)

  • convex/users.ts - User query functions
  • convex/schema.ts - User database schema

Usage

Basic Usage

import { CurrentUserAvatar } from "@/components/current-user-avatar";

export function Header() {
  return (
    <header>
      <h1>My App</h1>
      <CurrentUserAvatar />
    </header>
  );
}

Size Variants

import { CurrentUserAvatar } from "@/components/current-user-avatar";

// Small (32px)
<CurrentUserAvatar size="sm" />

// Medium (40px) - default
<CurrentUserAvatar size="md" />

// Large (48px)
<CurrentUserAvatar size="lg" />

Custom Styling

import { CurrentUserAvatar } from "@/components/current-user-avatar";

<CurrentUserAvatar 
  size="lg"
  className="ring-2 ring-primary ring-offset-2"
/>

Hide When Not Authenticated

import { CurrentUserAvatar } from "@/components/current-user-avatar";

<CurrentUserAvatar showFallback={false} />
This will render null if the user is not authenticated, instead of showing a ”?” fallback.

With Dropdown Menu

Combine with a dropdown for user actions:
import { CurrentUserAvatar } from "@/components/current-user-avatar";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useAuthActions } from "@convex-dev/auth/react";

export function UserMenu() {
  const { signOut } = useAuthActions();

  return (
    <DropdownMenu>
      <DropdownMenuTrigger>
        <CurrentUserAvatar size="md" />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => navigate({ to: "/profile" })}>
          Profile
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => navigate({ to: "/settings" })}>
          Settings
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => signOut()}>
          Sign Out
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

In Navigation

import { CurrentUserAvatar } from "@/components/current-user-avatar";
import { useCurrentUserName } from "@/hooks/use-current-user-name";

export function Navigation() {
  const userName = useCurrentUserName();

  return (
    <nav className="flex items-center gap-4">
      <Link to="/">Home</Link>
      <Link to="/dashboard">Dashboard</Link>
      <div className="ml-auto flex items-center gap-3">
        <span className="text-sm text-muted-foreground">
          {userName ?? "Guest"}
        </span>
        <CurrentUserAvatar size="sm" />
      </div>
    </nav>
  );
}

API Reference

CurrentUserAvatar Props

interface CurrentUserAvatarProps {
  size?: "sm" | "md" | "lg";
  className?: string;
  showFallback?: boolean;
}
size
string
default:"md"
Size variant for the avatar:
  • "sm" - 32px (h-8 w-8)
  • "md" - 40px (h-10 w-10)
  • "lg" - 48px (h-12 w-12)
className
string
Additional CSS classes to apply to the avatar
showFallback
boolean
default:true
Whether to show a fallback ”?” when user is not authenticated. If false, renders null when no user.

Hooks

useCurrentUserImage

Get the current user’s profile image URL:
import { useCurrentUserImage } from "@/hooks/use-current-user-image";

function MyComponent() {
  const image = useCurrentUserImage();
  // image: string | null | undefined
  // undefined = loading, null = no user/no image, string = image URL

  if (image === undefined) return <div>Loading...</div>;
  if (!image) return <div>No image</div>;

  return <img src={image} alt="User" />;
}

useCurrentUserName

Get the current user’s name:
import { useCurrentUserName } from "@/hooks/use-current-user-name";

function MyComponent() {
  const name = useCurrentUserName();
  // name: string | null | undefined
  // undefined = loading, null = no user, string = user name

  return <div>Hello, {name ?? "Guest"}!</div>;
}

Backend Implementation

User Query

The component uses a reactive Convex query:
convex/users.ts
import { query } from "./_generated/server";
import { getAuthUserId } from "@convex-dev/auth/server";
import { v } from "convex/values";

export const current = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) return null;
    return await ctx.db.get(userId);
  },
});
This query:
  • Returns the current authenticated user’s profile
  • Returns null if not authenticated
  • Is reactive - automatically updates when user data changes
  • Only exposes the user’s own data for security

User Schema

convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    name: v.optional(v.string()),
    email: v.optional(v.string()),
    image: v.optional(v.string()),
    // ... other fields
  }),
});

States

Loading State

Shows a skeleton while fetching user data:
// Automatically handled by the component
<CurrentUserAvatar /> // Shows skeleton when loading

Not Authenticated

Shows ”?” fallback by default:
// User not logged in
<CurrentUserAvatar /> // Shows "?" in avatar

// Or hide entirely
<CurrentUserAvatar showFallback={false} /> // Renders null

No Profile Image

Shows user initials when no image is set:
// User: { name: "John Doe", image: null }
<CurrentUserAvatar /> // Shows "JD"

// User: { name: "Alice", image: null }
<CurrentUserAvatar /> // Shows "A"

With Profile Image

Displays the user’s image:
// User: { name: "John Doe", image: "https://..." }
<CurrentUserAvatar /> // Shows profile picture

Customization

Custom Initials Logic

Modify the getInitials function in the component:
function getInitials(name: string | null): string {
  if (!name) return "?";
  
  // Custom: Use first letter only
  return name[0].toUpperCase();
  
  // Custom: Use full first name
  return name.split(" ")[0].toUpperCase();
}

Custom Sizes

Add more size variants:
const sizeClasses = {
  xs: "h-6 w-6 text-xs",
  sm: "h-8 w-8 text-xs",
  md: "h-10 w-10 text-sm",
  lg: "h-12 w-12 text-base",
  xl: "h-16 w-16 text-lg",
};

Custom Fallback

Change what shows when there’s no image:
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-pink-500">
  {getInitials(name)}
</AvatarFallback>

Examples

Profile Card

import { CurrentUserAvatar } from "@/components/current-user-avatar";
import { useCurrentUserName } from "@/hooks/use-current-user-name";
import { useQuery } from "@tanstack/react-query";
import { convexQuery, api } from "@/lib/convex/server";

export function ProfileCard() {
  const { data: user } = useQuery(convexQuery(api.users.current, {}));
  const name = useCurrentUserName();

  return (
    <div className="flex items-center gap-4 p-4 rounded-lg border">
      <CurrentUserAvatar size="lg" />
      <div>
        <h3 className="font-semibold">{name ?? "Anonymous"}</h3>
        <p className="text-sm text-muted-foreground">{user?.email}</p>
      </div>
    </div>
  );
}

Comment Author

import { CurrentUserAvatar } from "@/components/current-user-avatar";
import { useCurrentUserName } from "@/hooks/use-current-user-name";

export function CommentForm() {
  const userName = useCurrentUserName();

  return (
    <div className="flex gap-3">
      <CurrentUserAvatar size="sm" />
      <div className="flex-1">
        <p className="text-sm font-medium mb-2">{userName}</p>
        <textarea 
          placeholder="Write a comment..."
          className="w-full p-2 border rounded"
        />
      </div>
    </div>
  );
}

User List

import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";

function getInitials(name: string) {
  return name.split(" ").map(n => n[0]).join("").toUpperCase().slice(0, 2);
}

export function UserList({ users }: { users: Array<{ name: string; image?: string }> }) {
  return (
    <div className="space-y-2">
      {users.map((user) => (
        <div key={user.name} className="flex items-center gap-3">
          <Avatar className="h-10 w-10">
            <AvatarImage src={user.image} />
            <AvatarFallback>{getInitials(user.name)}</AvatarFallback>
          </Avatar>
          <span>{user.name}</span>
        </div>
      ))}
    </div>
  );
}

Troubleshooting

  • Ensure Convex is running (npx convex dev)
  • Check that ConvexClientProvider wraps your app
  • Verify the user query works in Convex dashboard
  • Check that the image URL is valid and accessible
  • Verify the user has an image field in the database
  • Check browser console for CORS errors
  • Ensure the user has a name field in the database
  • Check that authentication is working
Run npx convex dev to generate types from your schema.

Build docs developers (and LLMs) love