Floating Button

A floating button is a common UI component used in user interfaces. It typically appears as a button "floating" over other content, often positioned in a corner of the screen. This button allows users to quickly perform a key action. It is also known as a Floating Action Button (FAB).

Installation

Install the following dependencies:

npm i framer-motion clsx tailwind-merge usehooks-ts

Add util file

import { ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Copy and paste the following code into your project.

'use client';

import { ReactNode, useRef, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { useOnClickOutside } from 'usehooks-ts';

type FloatingButtonProps = {
  className?: string;
  children: ReactNode;
  triggerContent: ReactNode;
};

type FloatingButtonItemProps = {
  children: ReactNode;
};

const list = {
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
      staggerDirection: -1
    }
  },
  hidden: {
    opacity: 0,
    transition: {
      when: 'afterChildren',
      staggerChildren: 0.1
    }
  }
};

const item = {
  visible: { opacity: 1, y: 0 },
  hidden: { opacity: 0, y: 5 }
};

const btn = {
  visible: { rotate: '45deg' },
  hidden: { rotate: 0 }
};

function FloatingButton({ className, children, triggerContent }: FloatingButtonProps) {
  const ref = useRef(null);
  const [isOpen, setIsOpen] = useState(false);

  useOnClickOutside(ref, () => setIsOpen(false));

  return (
    <div className="flex flex-col items-center relative">
      <AnimatePresence>
        <motion.ul
          className="flex flex-col items-center absolute bottom-14 gap-2"
          initial="hidden"
          animate={isOpen ? 'visible' : 'hidden'}
          variants={list}>
          {children}
        </motion.ul>
        <motion.div
          variants={btn}
          animate={isOpen ? 'visible' : 'hidden'}
          ref={ref}
          onClick={() => setIsOpen(!isOpen)}>
          {triggerContent}
        </motion.div>
      </AnimatePresence>
    </div>
  );
}

function FloatingButtonItem({ children }: FloatingButtonItemProps) {
  return <motion.li variants={item}>{children}</motion.li>;
}

export { FloatingButton, FloatingButtonItem };

Usage

import { FloatingButton, FloatingButtonItem } from '@/components/ui/floating-button';
import { cn } from '@/lib/utils';
import { DribbbleIcon, FacebookIcon, LinkedinIcon, PlusIcon } from 'lucide-react';

export default function FloatingButtonExample() {
  const items = [
    {
      icon: <FacebookIcon />,
      bgColor: 'bg-[#1877f2]'
    },
    {
      icon: <DribbbleIcon />,
      bgColor: 'bg-[#ea4c89]'
    },
    {
      icon: <LinkedinIcon />,
      bgColor: 'bg-[#0a66c2]'
    }
  ];

  return (
    <FloatingButton
      triggerContent={
        <button className="flex items-center justify-center h-12 w-12 rounded-full bg-black dark:bg-slate-800 text-white/80 z-10">
          <PlusIcon />
        </button>
      }>
      {items.map((item, key) => (
        <FloatingButtonItem key={key}>
          <button
            className={cn(
              'h-12 w-12 rounded-full flex items-center justify-center text-white/80',
              item.bgColor
            )}>
            {item.icon}
          </button>
        </FloatingButtonItem>
      ))}
    </FloatingButton>
  );
}