Native Avatar With Name

PreviousNext

Avatar component that displays a name tooltip on hover with directional animations.

Docs
uitripledcomponent

Preview

Loading preview…
components/native/shadcnui/native-avatar-with-name-shadcnui.tsx
"use client";

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
import { useState } from "react";

export interface NativeAvatarProps {
  /**
   * URL of the avatar image
   */
  src?: string;
  /**
   * Name to display on hover
   */
  name: string;
  /**
   * Fallback text when image fails to load (usually initials)
   */
  fallback?: string;
  /**
   * Size variant of the avatar
   * Default: "md"
   */
  size?: "sm" | "md" | "lg" | "xl";
  /**
   * Direction from which the name appears
   * Default: "bottom"
   */
  direction?: "top" | "bottom" | "left" | "right";
  /**
   * Additional class names for the container
   */
  className?: string;
  /**
   * Additional class names for the name label
   */
  nameClassName?: string;
  /**
   * Additional class names for the motion container
   */
  motionClassName?: string;
}

const sizeVariants = {
  sm: "h-10 w-10",
  md: "h-14 w-14",
  lg: "h-20 w-20",
  xl: "h-28 w-28",
};

const nameSizeVariants = {
  sm: "text-xs px-2 py-1",
  md: "text-sm px-3 py-1.5",
  lg: "text-base px-4 py-2",
  xl: "text-lg px-5 py-2.5",
};

export function NativeAvatarWithName({
  src,
  name,
  fallback,
  size = "md",
  direction = "bottom",
  className,
  nameClassName,
  motionClassName,
}: NativeAvatarProps) {
  const [isHovered, setIsHovered] = useState(false);

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

  const directionVariants = {
    top: {
      initial: { y: 20, opacity: 0, filter: "blur(4px)" },
      animate: { y: -8, opacity: 1, filter: "blur(0px)" },
      exit: { y: 20, opacity: 0, filter: "blur(4px)" },
    },
    bottom: {
      initial: { y: -20, opacity: 0, filter: "blur(4px)" },
      animate: { y: 8, opacity: 1, filter: "blur(0px)" },
      exit: { y: -20, opacity: 0, filter: "blur(4px)" },
    },
    left: {
      initial: { x: 20, opacity: 0, filter: "blur(4px)" },
      animate: { x: -8, opacity: 1, filter: "blur(0px)" },
      exit: { x: 20, opacity: 0, filter: "blur(4px)" },
    },
    right: {
      initial: { x: -20, opacity: 0, filter: "blur(4px)" },
      animate: { x: 8, opacity: 1, filter: "blur(0px)" },
      exit: { x: -20, opacity: 0, filter: "blur(4px)" },
    },
  };

  const positionClasses = {
    top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
    bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
    left: "right-full top-1/2 -translate-y-1/2 mr-2",
    right: "left-full top-1/2 -translate-y-1/2 ml-2",
  };

  return (
    <div
      className={cn(
        "relative inline-flex items-center justify-center",
        className
      )}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
    >
      <motion.div
        whileHover={{ scale: 1.05 }}
        whileTap={{ scale: 0.95 }}
        transition={{ type: "spring", stiffness: 400, damping: 17 }}
        className={motionClassName}
      >
        <Avatar
          className={cn(
            sizeVariants[size],
            "cursor-pointer ring-2 ring-background shadow-lg"
          )}
        >
          <AvatarImage src={src || "/placeholder.svg"} alt={name} />
          <AvatarFallback className="text-muted-foreground font-semibold">
            {fallback || getInitials(name)}
          </AvatarFallback>
        </Avatar>
      </motion.div>

      <AnimatePresence>
        {isHovered && (
          <motion.div
            initial={directionVariants[direction].initial}
            animate={directionVariants[direction].animate}
            exit={directionVariants[direction].exit}
            transition={{
              type: "spring",
              stiffness: 300,
              damping: 25,
              opacity: { duration: 0.2 },
              filter: { duration: 0.2 },
            }}
            className={cn(
              "absolute z-10 whitespace-nowrap rounded-md bg-popover text-popover-foreground shadow-lg border pointer-events-none",
              nameSizeVariants[size],
              positionClasses[direction],
              nameClassName
            )}
          >
            <span className="font-medium">{name}</span>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

Installation

npx shadcn@latest add @uitripled/native-avatar-with-name

Usage

import { NativeAvatarWithName } from "@/components/native-avatar-with-name"
<NativeAvatarWithName />