Todos os componentes

Notification Badge

Notification Badge é um componente de feedback para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.

SmoothUI MIT feedback

O que é

Notification Badge é um componente de feedback para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Precisa de

npm i motion

Licença

Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Notification Badge

index.tsx
"use client";

import { cn } from "@/lib/utils";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import type { ReactNode } from "react";
import { useEffect, useRef, useState } from "react";

export interface NotificationBadgeProps {
  children?: ReactNode;
  className?: string;
  count?: number;
  max?: number;
  ping?: boolean;
  position?: "top-right" | "top-left" | "bottom-right" | "bottom-left";
  showZero?: boolean;
  status?: "online" | "offline" | "busy" | "away";
  variant?: "dot" | "count" | "status";
}

const statusColors = {
  away: "bg-amber-500",
  busy: "bg-red-500",
  offline: "bg-gray-400",
  online: "bg-emerald-500",
};

const positionClasses = {
  "bottom-left": "-bottom-1 -left-1",
  "bottom-right": "-bottom-1 -right-1",
  "top-left": "-top-1 -left-1",
  "top-right": "-top-1 -right-1",
};

const AnimatedCount = ({
  value,
  max,
  shouldReduceMotion,
}: {
  value: number;
  max: number;
  shouldReduceMotion: boolean | null;
}) => {
  const displayValue = value > max ? `${max}+` : value.toString();
  const prevValueRef = useRef(value);
  const direction = value > prevValueRef.current ? 1 : -1;

  useEffect(() => {
    prevValueRef.current = value;
  }, [value]);

  if (shouldReduceMotion) {
    return <span className="font-medium leading-none">{displayValue}</span>;
  }

  return (
    <span className="relative overflow-hidden font-medium leading-none">
      <AnimatePresence initial={false} mode="popLayout">
        <motion.span
          animate={{ opacity: 1, y: 0 }}
          className="inline-block"
          exit={{ opacity: 0, y: direction * -12 }}
          initial={{ opacity: 0, y: direction * 12 }}
          key={value}
          transition={{ bounce: 0.1, duration: 0.3, type: "spring" as const }}
        >
          {displayValue}
        </motion.span>
      </AnimatePresence>
    </span>
  );
};

const NotificationBadge = ({
  variant = "dot",
  count = 0,
  max = 99,
  status = "online",
  showZero = false,
  ping = false,
  position = "top-right",
  children,
  className,
}: NotificationBadgeProps) => {
  const shouldReduceMotion = useReducedMotion();
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const shouldShow =
      variant === "dot" ||
      variant === "status" ||
      (variant === "count" && (count > 0 || showZero));
    setIsVisible(shouldShow);
  }, [variant, count, showZero]);

  const getBadgeClasses = () => {
    if (variant === "dot") {
      return "h-2.5 w-2.5";
    }
    if (variant === "status") {
      return "h-3 w-3";
    }
    const displayValue = count > max ? `${max}+` : count.toString();
    if (displayValue.length === 1) {
      return "h-5 w-5 text-xs";
    }
    if (displayValue.length === 2) {
      return "h-5 min-w-5 px-1 text-xs";
    }
    return "h-5 min-w-6 px-1 text-xs";
  };

  const getBackgroundColor = () => {
    if (variant === "status") {
      return statusColors[status];
    }
    return "bg-brand";
  };

  const badgeElement = (
    <AnimatePresence mode="wait">
      {isVisible ? (
        <motion.span
          animate={{ opacity: 1, scale: 1 }}
          className={cn(
            "absolute flex items-center justify-center rounded-full text-white",
            getBackgroundColor(),
            getBadgeClasses(),
            positionClasses[position],
            variant === "status" && "ring-2 ring-white dark:ring-gray-900",
            className
          )}
          exit={
            shouldReduceMotion
              ? { opacity: 0, transition: { duration: 0 } }
              : { opacity: 0, scale: 0, transition: { duration: 0.15 } }
          }
          initial={
            shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0 }
          }
          transition={
            shouldReduceMotion
              ? { duration: 0 }
              : { bounce: 0.2, duration: 0.25, type: "spring" as const }
          }
        >
          {variant === "count" && (
            <AnimatedCount
              max={max}
              shouldReduceMotion={shouldReduceMotion}
              value={count}
            />
          )}

          {ping && !shouldReduceMotion && (
            <span
              aria-hidden="true"
              className={cn(
                "absolute inset-0 animate-ping rounded-full opacity-75",
                getBackgroundColor()
              )}
            />
          )}
        </motion.span>
      ) : null}
    </AnimatePresence>
  );

  if (!children) {
    return (
      <span className="relative inline-flex">
        <span className="h-4 w-4" />
        {badgeElement}
      </span>
    );
  }

  return (
    <span className="relative inline-flex">
      {children}
      {badgeElement}
    </span>
  );
};

export default NotificationBadge;

Componentes parecidos