Web InnoventixFreeCode

Badge Pop Hover Effect

Original · free

Badges and pills that pop and change colour on hover.

byWeb InnoventixReact + Tailwind
hoverbadgepop
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/hover-badge-pop.json
hover-badge-pop.tsx
"use client";

import { useId, useState } from "react";

type Badge = {
  label: string;
  count?: number;
  base: string;
  hover: string;
  ring: string;
  dot: string;
};

const badges: Badge[] = [
  {
    label: "New",
    base: "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-300",
    hover:
      "hover:border-emerald-500 hover:bg-emerald-600 hover:text-white dark:hover:bg-emerald-500 dark:hover:text-white dark:hover:border-emerald-400",
    ring: "focus-visible:ring-emerald-500/50",
    dot: "bg-emerald-500 group-hover/badge:bg-white",
  },
  {
    label: "Trending",
    count: 24,
    base: "border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-300",
    hover:
      "hover:border-rose-500 hover:bg-rose-600 hover:text-white dark:hover:bg-rose-500 dark:hover:text-white dark:hover:border-rose-400",
    ring: "focus-visible:ring-rose-500/50",
    dot: "bg-rose-500 group-hover/badge:bg-white",
  },
  {
    label: "Beta",
    base: "border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-300",
    hover:
      "hover:border-amber-500 hover:bg-amber-500 hover:text-white dark:hover:bg-amber-500 dark:hover:text-white dark:hover:border-amber-400",
    ring: "focus-visible:ring-amber-500/50",
    dot: "bg-amber-500 group-hover/badge:bg-white",
  },
  {
    label: "Pro",
    base: "border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-500/30 dark:bg-violet-500/10 dark:text-violet-300",
    hover:
      "hover:border-violet-500 hover:bg-violet-600 hover:text-white dark:hover:bg-violet-500 dark:hover:text-white dark:hover:border-violet-400",
    ring: "focus-visible:ring-violet-500/50",
    dot: "bg-violet-500 group-hover/badge:bg-white",
  },
  {
    label: "Stable",
    count: 8,
    base: "border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-500/30 dark:bg-sky-500/10 dark:text-sky-300",
    hover:
      "hover:border-sky-500 hover:bg-sky-600 hover:text-white dark:hover:bg-sky-500 dark:hover:text-white dark:hover:border-sky-400",
    ring: "focus-visible:ring-sky-500/50",
    dot: "bg-sky-500 group-hover/badge:bg-white",
  },
  {
    label: "Featured",
    base: "border-indigo-200 bg-indigo-50 text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300",
    hover:
      "hover:border-indigo-500 hover:bg-indigo-600 hover:text-white dark:hover:bg-indigo-500 dark:hover:text-white dark:hover:border-indigo-400",
    ring: "focus-visible:ring-indigo-500/50",
    dot: "bg-indigo-500 group-hover/badge:bg-white",
  },
];

const filters = [
  "Design",
  "Engineering",
  "Product",
  "Research",
  "Marketing",
  "Support",
  "Finance",
  "Operations",
];

export default function HoverBadgePop() {
  const groupId = useId();
  const [active, setActive] = useState<Set<string>>(new Set(["Design", "Product"]));

  const toggle = (name: string) => {
    setActive((prev) => {
      const next = new Set(prev);
      if (next.has(name)) next.delete(name);
      else next.add(name);
      return next;
    });
  };

  return (
    <section className="relative w-full bg-white px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:py-24">
      <style>{`
        @keyframes hbp-pop-in {
          0% { transform: scale(0.6); opacity: 0; }
          70% { transform: scale(1.08); opacity: 1; }
          100% { transform: scale(1); opacity: 1; }
        }
        .hbp-pop-in { animation: hbp-pop-in 240ms cubic-bezier(0.34, 1.56, 0.64, 1) both; }
        @media (prefers-reduced-motion: reduce) {
          .hbp-pop-in { animation: none; }
          .hbp-badge, .hbp-badge * { transition: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-4xl">
        <div className="mb-12 text-center">
          <span className="inline-block rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium uppercase tracking-wider text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
            Interactive
          </span>
          <h2 className="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">
            Badges that pop on hover
          </h2>
          <p className="mx-auto mt-3 max-w-xl text-base text-slate-600 dark:text-slate-400">
            Hover or focus any pill. It scales up and shifts to a bolder colour with a smooth,
            spring-like lift.
          </p>
        </div>

        {/* Status badges */}
        <div className="mb-14">
          <h3 className="mb-4 text-sm font-semibold text-slate-500 dark:text-slate-400">
            Status labels
          </h3>
          <div className="flex flex-wrap gap-3">
            {badges.map((b) => (
              <a
                key={b.label}
                href="#"
                onClick={(e) => e.preventDefault()}
                className={`hbp-badge group/badge inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium shadow-sm outline-none transition-all duration-200 ease-out hover:-translate-y-0.5 hover:scale-105 hover:shadow-md focus-visible:-translate-y-0.5 focus-visible:scale-105 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 ${b.base} ${b.hover} ${b.ring}`}
              >
                <span
                  className={`h-1.5 w-1.5 rounded-full transition-colors duration-200 ${b.dot}`}
                  aria-hidden="true"
                />
                {b.label}
                {typeof b.count === "number" && (
                  <span className="tabular-nums opacity-70 transition-opacity duration-200 group-hover/badge:opacity-100">
                    {b.count}
                  </span>
                )}
              </a>
            ))}
          </div>
        </div>

        {/* Selectable filter chips */}
        <div>
          <h3 className="mb-4 text-sm font-semibold text-slate-500 dark:text-slate-400">
            Toggle filters
            <span className="ml-2 font-normal text-slate-400 dark:text-slate-500">
              ({active.size} selected)
            </span>
          </h3>
          <div role="group" aria-label="Team filters" className="flex flex-wrap gap-2.5">
            {filters.map((name) => {
              const on = active.has(name);
              return (
                <button
                  key={name}
                  type="button"
                  aria-pressed={on}
                  onClick={() => toggle(name)}
                  className={`hbp-badge group/chip inline-flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-sm font-medium outline-none transition-all duration-200 ease-out hover:-translate-y-0.5 hover:scale-105 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500/50 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 ${
                    on
                      ? "border-indigo-500 bg-indigo-600 text-white shadow-md shadow-indigo-500/20 hover:bg-indigo-500 dark:border-indigo-400 dark:bg-indigo-500 dark:hover:bg-indigo-400"
                      : "border-slate-200 bg-white text-slate-600 shadow-sm hover:border-indigo-300 hover:bg-indigo-50 hover:text-indigo-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-indigo-500/50 dark:hover:bg-indigo-500/10 dark:hover:text-indigo-300"
                  }`}
                >
                  <span
                    key={`${groupId}-${name}-${on}`}
                    className={`hbp-pop-in flex h-4 w-4 items-center justify-center rounded-full text-[10px] ${
                      on
                        ? "bg-white/25 text-white"
                        : "bg-slate-100 text-slate-400 dark:bg-slate-800 dark:text-slate-500"
                    }`}
                    aria-hidden="true"
                  >
                    {on ? (
                      <svg
                        viewBox="0 0 12 12"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth="2"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        className="h-2.5 w-2.5"
                      >
                        <path d="M2.5 6.5 5 9l4.5-5" />
                      </svg>
                    ) : (
                      <svg
                        viewBox="0 0 12 12"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth="2"
                        strokeLinecap="round"
                        className="h-2.5 w-2.5"
                      >
                        <path d="M6 2.5v7M2.5 6h7" />
                      </svg>
                    )}
                  </span>
                  {name}
                </button>
              );
            })}
          </div>
        </div>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →