Popover

Component

Elegant Tailwind popover dialogs redesigned with refined glassmorphism. A perfect Tailwind CSS tooltip alternative for complex interactions.

Install via CLI

Run this command to automatically add the component and its dependencies to your project.

npx @abhaysinghr516/business-wish add popover
New to the CLI? Run npx @abhaysinghr516/business-wish init first to initialize your project.

For contextual actions that require more space than a tooltip but less commitment than a full modal, the Tailwind popover is your solution. Serving as a robust Tailwind CSS tooltip alternative, these popovers handle rich profiles, command palettes, and notification hubs with fluid animations.

If you only need to display a simple list of actions, consider a Dropdown instead, or use a full Modal if the interaction forces an interruption.

Basic Settings Popover

import React, { useState, useRef, useEffect } from "react";
import { Settings2, X } from "lucide-react";

// Hook for handling clicks outside
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);
    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [ref, handler]);
}

const BasicPopover = () => {
  const [isOpen, setIsOpen] = useState(false);
  const popoverRef = useRef(null);
  
  useOnClickOutside(popoverRef, () => setIsOpen(false));

  return (
    <div className="relative inline-block" ref={popoverRef}>
      <button
        onClick={() => setIsOpen(!isOpen)}
        className={`p-2.5 rounded-xl transition-all duration-300 ${
          isOpen
            ? "bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-white"
            : "bg-white dark:bg-neutral-950 text-neutral-600 dark:text-neutral-400 border border-neutral-200 dark:border-white/10 hover:bg-neutral-50 dark:hover:bg-neutral-900 shadow-sm"
        }`}
      >
        <Settings2 className="w-5 h-5" />
      </button>

      <div
        className={`absolute top-full mt-2 -left-20 w-72 z-50 transition-all duration-300 origin-top-right ${
          isOpen ? "opacity-100 scale-100 translate-y-0 visible" : "opacity-0 scale-95 -translate-y-2 invisible"
        }`}
      >
        <div className="bg-white/90 dark:bg-neutral-900/90 backdrop-blur-xl border border-neutral-200/50 dark:border-white/10 rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.08)] dark:shadow-[0_8px_30px_rgb(0,0,0,0.4)] overflow-hidden">
          <div className="p-4 border-b border-neutral-200/50 dark:border-white/5">
            <div className="flex items-center justify-between">
              <h3 className="font-semibold text-[15px] text-neutral-900 dark:text-white">Settings</h3>
              <button onClick={() => setIsOpen(false)} className="p-1 rounded-full text-neutral-400 hover:text-neutral-900 dark:hover:text-white bg-neutral-100 dark:bg-neutral-800 transition-colors">
                <X className="w-4 h-4" />
              </button>
            </div>
            <p className="text-[13px] text-neutral-500 mt-1 leading-relaxed">
              Manage your application preferences and general configurations.
            </p>
          </div>
          <div className="p-2 flex flex-col gap-1">
            {["General", "Appearance", "Advanced"].map((item) => (
              <button key={item} className="px-3 py-2 text-[14px] text-left text-neutral-600 dark:text-neutral-300 font-medium rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:text-neutral-900 dark:hover:text-white transition-colors">
                {item}
              </button>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
};

export default BasicPopover;
import React, { useState, useRef, useEffect } from "react";
import { User, Settings2, CreditCard, LogOut } from "lucide-react";

// Hook for handling clicks outside
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);
    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [ref, handler]);
}

const MenuPopover = () => {
  const [isOpen, setIsOpen] = useState(false);
  const popoverRef = useRef(null);
  
  useOnClickOutside(popoverRef, () => setIsOpen(false));

  return (
    <div className="relative inline-block" ref={popoverRef}>
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center gap-2 px-3 py-2 bg-white dark:bg-neutral-950 border border-neutral-200 dark:border-white/10 rounded-full hover:bg-neutral-50 dark:hover:bg-neutral-900 transition-all shadow-sm"
      >
        <div className="w-6 h-6 rounded-full bg-gradient-to-tr from-blue-500 to-purple-500 flex items-center justify-center shadow-inner">
          <User className="w-3.5 h-3.5 text-white" />
        </div>
        <span className="text-[14px] font-medium text-neutral-700 dark:text-neutral-200 pl-1 pr-2">Olivia P.</span>
      </button>

      <div
        className={`absolute top-full mt-2 left-0 w-60 z-50 transition-all duration-300 origin-top-left ${
          isOpen ? "opacity-100 scale-100 translate-y-0 visible" : "opacity-0 scale-95 -translate-y-2 invisible"
        }`}
      >
        <div className="bg-white/80 dark:bg-neutral-950/80 backdrop-blur-2xl border border-neutral-200/50 dark:border-white/10 rounded-2xl shadow-xl overflow-hidden p-1.5">
          <div className="flex flex-col gap-0.5">
            <button className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-neutral-100/80 dark:hover:bg-neutral-900/80 text-neutral-600 dark:text-neutral-300 transition-colors w-full text-left">
              <User className="w-4 h-4 text-neutral-400" />
              <span className="text-[14px] font-medium">My Account</span>
            </button>
            <button className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-neutral-100/80 dark:hover:bg-neutral-900/80 text-neutral-600 dark:text-neutral-300 transition-colors w-full text-left">
              <CreditCard className="w-4 h-4 text-neutral-400" />
              <span className="text-[14px] font-medium">Billing</span>
            </button>
            <button className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-neutral-100/80 dark:hover:bg-neutral-900/80 text-neutral-600 dark:text-neutral-300 transition-colors w-full text-left">
              <Settings2 className="w-4 h-4 text-neutral-400" />
              <span className="text-[14px] font-medium">Preferences</span>
            </button>
          </div>
          
          <div className="h-px w-full bg-neutral-200/60 dark:bg-neutral-800/60 my-1.5" />
          
          <button className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-red-50 dark:hover:bg-red-500/10 text-red-600 dark:text-red-400 transition-colors w-full text-left">
            <LogOut className="w-4 h-4" />
            <span className="text-[14px] font-medium">Sign Out</span>
          </button>
        </div>
      </div>
    </div>
  );
};

export default MenuPopover;

Notification Popover

import React, { useState, useRef, useEffect } from "react";
import { Bell } from "lucide-react";

// Hook for handling clicks outside
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);
    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [ref, handler]);
}

const NotificationPopover = () => {
  const [isOpen, setIsOpen] = useState(false);
  const popoverRef = useRef(null);
  
  useOnClickOutside(popoverRef, () => setIsOpen(false));

  return (
    <div className="relative inline-block" ref={popoverRef}>
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="relative p-2.5 bg-white dark:bg-neutral-950 border border-neutral-200 dark:border-white/10 rounded-full hover:bg-neutral-50 dark:hover:bg-neutral-900 transition-all shadow-sm"
      >
        <Bell className="w-5 h-5 text-neutral-600 dark:text-neutral-300" />
        <span className="absolute top-2.5 right-2.5 w-2 h-2 bg-blue-500 border-2 border-white dark:border-neutral-950 rounded-full" />
      </button>

      <div
        className={`absolute top-full mt-2 -left-36 w-80 z-50 transition-all duration-300 origin-top ${
          isOpen ? "opacity-100 scale-100 translate-y-0 visible" : "opacity-0 scale-95 -translate-y-2 invisible"
        }`}
      >
        <div className="bg-white/95 dark:bg-neutral-900/95 backdrop-blur-xl border border-neutral-200/50 dark:border-white/10 rounded-3xl shadow-xl overflow-hidden">
          <div className="px-5 py-4 flex items-center justify-between border-b border-neutral-200/50 dark:border-white/5">
            <h3 className="font-semibold text-[15px] text-neutral-900 dark:text-white">Notifications</h3>
            <span className="text-[12px] font-medium text-blue-500 bg-blue-500/10 px-2 py-0.5 rounded-full">2 New</span>
          </div>
          
          <div className="max-h-[300px] overflow-y-auto p-2">
            {[
              { title: "Design System Update", desc: "v2.0 is now available.", time: "2m ago", unread: true },
              { title: "Weekly Report", desc: "Your analytics report is ready.", time: "1h ago", unread: true },
              { title: "Security Alert", desc: "New login from unauthorized device.", time: "2d ago", unread: false }
            ].map((noti, i) => (
              <div key={i} className="flex gap-4 p-3 rounded-2xl hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50 transition-colors cursor-pointer group">
                <div className="mt-1">
                  <div className={`w-2 h-2 rounded-full ${noti.unread ? "bg-blue-500" : "bg-transparent"}`} />
                </div>
                <div className="flex-1">
                  <div className="flex items-center justify-between">
                    <p className={`text-[14px] ${noti.unread ? "font-semibold text-neutral-900 dark:text-white" : "font-medium text-neutral-700 dark:text-neutral-300"}`}>{noti.title}</p>
                    <span className="text-[12px] text-neutral-400">{noti.time}</span>
                  </div>
                  <p className="text-[13px] text-neutral-500 mt-0.5 pr-4">{noti.desc}</p>
                </div>
              </div>
            ))}
          </div>
          
          <div className="p-2 border-t border-neutral-200/50 dark:border-white/5">
            <button className="w-full py-2 text-[13px] font-medium text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-xl transition-colors">
              Mark all as read
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default NotificationPopover;

Rich Profile Popover

import React, { useState, useRef, useEffect } from "react";
import { Shield, CreditCard, LogOut } from "lucide-react";

// Hook for handling clicks outside
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);
    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [ref, handler]);
}

const RichProfilePopover = () => {
  const [isOpen, setIsOpen] = useState(false);
  const popoverRef = useRef(null);
  
  useOnClickOutside(popoverRef, () => setIsOpen(false));

  return (
    <div className="relative inline-block" ref={popoverRef}>
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="group relative w-12 h-12 rounded-full border-2 border-transparent hover:border-neutral-200 dark:hover:border-neutral-800 transition-all focus:outline-none"
      >
        <img 
          src="https://i.pravatar.cc/150?img=32" 
          alt="Profile" 
          className="w-full h-full rounded-full object-cover shadow-sm group-hover:scale-95 transition-transform"
        />
        <div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-white dark:border-neutral-950 rounded-full" />
      </button>

      <div
        className={`absolute top-full mt-3 -left-24 w-80 z-50 transition-all duration-400 origin-top ${
          isOpen ? "opacity-100 scale-100 translate-y-0 visible" : "opacity-0 scale-95 -translate-y-4 invisible"
        }`}
      >
        <div className="bg-white/95 dark:bg-[#111] backdrop-blur-3xl rounded-[28px] shadow-2xl overflow-hidden border border-neutral-200/60 dark:border-white/10 ring-1 ring-black/5 dark:ring-white/5 p-2">
          
          {/* Header Card */}
          <div className="bg-neutral-100/50 dark:bg-neutral-900 p-4 rounded-2xl flex items-center gap-4 mb-2">
            <img src="https://i.pravatar.cc/150?img=32" className="w-14 h-14 rounded-full border border-neutral-200 dark:border-neutral-800" alt="Avatar"/>
            <div>
              <h3 className="text-[16px] font-semibold text-neutral-900 dark:text-white leading-tight">Sarah Jenkins</h3>
              <p className="text-[13px] text-neutral-500 font-medium">Product Designer</p>
              <div className="mt-1.5 flex items-center gap-1.5">
                <span className="flex items-center justify-center w-5 h-5 rounded bg-blue-100 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400 text-[10px] font-bold">Pro</span>
                <span className="text-[12px] text-neutral-400">sarah@apple.com</span>
              </div>
            </div>
          </div>

          {/* Action Grid */}
          <div className="grid grid-cols-2 gap-1 mb-2">
            <button className="flex flex-col items-center justify-center gap-1.5 p-3 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800/50 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors">
              <Shield className="w-5 h-5" strokeWidth={1.5} />
              <span className="text-[12px] font-medium">Security</span>
            </button>
            <button className="flex flex-col items-center justify-center gap-1.5 p-3 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800/50 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors">
              <CreditCard className="w-5 h-5" strokeWidth={1.5} />
              <span className="text-[12px] font-medium">Billing</span>
            </button>
          </div>

          <div className="h-px bg-neutral-200/50 dark:bg-neutral-800/50 mx-2 mb-2" />

          {/* Footer Action */}
          <button className="w-full flex items-center justify-center gap-2 p-3 rounded-xl hover:bg-red-500/10 text-red-500 dark:text-red-400 transition-colors font-medium text-[14px]">
            <LogOut className="w-4 h-4" />
            Sign Out
          </button>
        </div>
      </div>
    </div>
  );
};

export default RichProfilePopover;

Command Style Popover

import React, { useState, useRef, useEffect } from "react";
import { Search, Settings2, Sun, User } from "lucide-react";

// Hook for handling clicks outside
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);
    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [ref, handler]);
}

const CommandPopover = () => {
  const [isOpen, setIsOpen] = useState(false);
  const popoverRef = useRef(null);
  
  useOnClickOutside(popoverRef, () => setIsOpen(false));

  return (
    <div className="relative inline-block" ref={popoverRef}>
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center gap-3 px-4 py-2.5 bg-neutral-100 dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl hover:border-neutral-300 dark:hover:border-neutral-700 transition-all text-[14px] text-neutral-500 w-[240px]"
      >
        <Search className="w-4 h-4" />
        <span>Search quickly...</span>
        <div className="ml-auto flex items-center gap-1">
          <kbd className="px-1.5 py-0.5 bg-white dark:bg-neutral-950 rounded text-[10px] font-semibold border border-neutral-200 dark:border-neutral-800 shadow-sm"></kbd>
          <kbd className="px-1.5 py-0.5 bg-white dark:bg-neutral-950 rounded text-[10px] font-semibold border border-neutral-200 dark:border-neutral-800 shadow-sm">K</kbd>
        </div>
      </button>

      <div
        className={`absolute top-full mt-2 left-0 w-[400px] z-50 transition-all duration-200 ${
          isOpen ? "opacity-100 scale-100 translate-y-0 visible" : "opacity-0 scale-95 translate-y-1 invisible pointer-events-none"
        }`}
      >
        <div className="bg-white/95 dark:bg-[#111]/95 backdrop-blur-2xl rounded-2xl shadow-[0_20px_40px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.4)] border border-neutral-200/50 dark:border-white/10 overflow-hidden flex flex-col">
          
          {/* Input Header */}
          <div className="flex items-center px-4 py-3 border-b border-neutral-200/50 dark:border-white/5">
            <Search className="w-4 h-4 text-neutral-400 mr-2" />
            <input 
              type="text" 
              placeholder="Type a command or search..."
              className="flex-1 bg-transparent border-none outline-none text-[14px] text-neutral-900 dark:text-white placeholder:text-neutral-400"
              autoFocus={isOpen}
            />
          </div>

          {/* List items */}
          <div className="p-2 max-h-[300px] overflow-y-auto w-full">
            <p className="px-2 py-1.5 text-[11px] font-semibold text-neutral-400 uppercase tracking-wider">Suggestions</p>
            
            <button className="flex items-center w-full gap-3 px-3 py-2.5 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800 text-neutral-700 dark:text-neutral-200 transition-colors group">
              <div className="w-6 h-6 rounded bg-neutral-200 dark:bg-neutral-900 flex items-center justify-center p-1 border border-neutral-300 dark:border-neutral-800 group-hover:bg-white dark:group-hover:bg-neutral-950 transition-colors">
                <Settings2 className="w-3.5 h-3.5" />
              </div>
              <span className="text-[13px] font-medium">Open Settings</span>
            </button>
            <button className="flex items-center w-full gap-3 px-3 py-2.5 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800 text-neutral-700 dark:text-neutral-200 transition-colors group">
              <div className="w-6 h-6 rounded bg-neutral-200 dark:bg-neutral-900 flex items-center justify-center p-1 border border-neutral-300 dark:border-neutral-800 group-hover:bg-white dark:group-hover:bg-neutral-950 transition-colors">
                <Sun className="w-3.5 h-3.5" />
              </div>
              <span className="text-[13px] font-medium">Toggle Light Theme</span>
            </button>
            <button className="flex items-center w-full gap-3 px-3 py-2.5 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800 text-neutral-700 dark:text-neutral-200 transition-colors group">
              <div className="w-6 h-6 rounded bg-neutral-200 dark:bg-neutral-900 flex items-center justify-center p-1 border border-neutral-300 dark:border-neutral-800 group-hover:bg-white dark:group-hover:bg-neutral-950 transition-colors">
                <User className="w-3.5 h-3.5" />
              </div>
              <span className="text-[13px] font-medium">View Profile</span>
            </button>
          </div>
          
          <div className="p-2 border-t border-neutral-200/50 dark:border-white/5 bg-neutral-50 dark:bg-black/20 flex items-center justify-between text-[11px] text-neutral-500 font-medium">
            <span className="flex items-center gap-1">Navigate with <span className="p-0.5 bg-neutral-200 dark:bg-neutral-800 rounded px-1"></span><span className="p-0.5 bg-neutral-200 dark:bg-neutral-800 rounded px-1"></span></span>
            <span className="flex items-center gap-1">Select with <span className="p-0.5 bg-neutral-200 dark:bg-neutral-800 rounded px-1"></span></span>
          </div>
        </div>
      </div>
    </div>
  );
};

export default CommandPopover;