Fiber UI LogoFiberUI

useClickOutside

A React hook that detects clicks outside of a specified element and triggers a callback. Perfect for closing modals, dropdowns, and popovers when interacting with other parts of the UI.

Installation

npx shadcn@latest add https://r.fiberui.com/r/hooks/use-click-outside.json

Features

  • Event Detection - Detects clicks outside of a specified element.
  • Customizable Events - Supports mousedown (default) or mouseup events.
  • Touch Support - Automatically handles touch events for mobile devices.

Source Code

View the full hook implementation in the Hook Source Code section below.

Basic Usage

The useClickOutside hook takes a ref to the element you want to monitor and a callback function to run when a click occurs outside that element.

"use client";

import { useClickOutside } from "@repo/hooks/dom/use-click-outside";
import { useRef, useState } from "react";

export function Example1() {
    const [isOpen, setIsOpen] = useState(false);
    const ref = useRef<HTMLDivElement>(null);

    useClickOutside(ref, () => {
        setIsOpen(false);
    });

    return (
        <div className="flex flex-col items-center gap-4 p-8">
            <button
                onClick={() => setIsOpen(true)}
                className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
            >
                {isOpen ? "Modal Open" : "Open Modal"}
            </button>

            {isOpen && (
                <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/20 backdrop-blur-sm">
                    <div
                        ref={ref}
                        className="bg-card text-card-foreground w-full max-w-sm rounded-lg border p-6 shadow-lg"
                    >
                        <h3 className="text-lg font-semibold">Modal Title</h3>
                        <p className="text-muted-foreground mt-2 text-sm">
                            Click outside this box to close it.
                        </p>
                    </div>
                </div>
            )}
        </div>
    );
}

Use Cases

Closing Modals/Dropdowns

The most common use case is closing a modal, dropdown, or popover when the user clicks somewhere else on the page.

Ignoring Specific Elements

You can ignore clicks on specific elements by checking the event target in your handler, or by ensuring the element you want to ignore is outside the ref (though typically you care about what's inside the ref).

Click outside me
(triggers count)

Outside Clicks: 0

"use client";

import { useClickOutside } from "@repo/hooks/dom/use-click-outside";
import { useRef, useState } from "react";

export function Example2() {
    const [count, setCount] = useState(0);
    const boxRef = useRef<HTMLDivElement>(null);
    const ignoreRef = useRef<HTMLButtonElement>(null);

    useClickOutside(boxRef, (event) => {
        // Example of ignoring a specific element manually if needed,
        // though typically you just put it inside the ref.
        // But here, the ignore button is outside the box.
        if (ignoreRef.current?.contains(event.target as Node)) {
            return;
        }
        setCount((c) => c + 1);
    });

    return (
        <div className="flex flex-col items-center gap-8 p-8">
            <div
                ref={boxRef}
                className="bg-secondary text-secondary-foreground flex h-32 w-32 items-center justify-center rounded-lg border shadow-sm"
            >
                <span className="text-center text-sm font-medium">
                    Click outside me
                    <br />
                    (triggers count)
                </span>
            </div>

            <div className="flex flex-col items-center gap-2">
                <p className="font-mono text-sm">Outside Clicks: {count}</p>
                <button
                    ref={ignoreRef}
                    className="bg-muted text-muted-foreground hover:bg-muted/80 rounded px-3 py-1 text-xs"
                >
                    I am ignored (won&apos;t trigger count)
                </button>
            </div>
        </div>
    );
}

API Reference

function useClickOutside<T extends HTMLElement>(
    ref: React.RefObject<T>,
    handler: (event: MouseEvent | TouchEvent) => void,
    mouseEvent?: "mousedown" | "mouseup",
): void;

Parameters

ParameterTypeDefaultDescription
refReact.RefObject<T>-A React ref object pointing to the element to monitor.
handler(event: MouseEvent | TouchEvent) => void-The callback function to execute when a click outside occurs.
mouseEvent"mousedown" | "mouseup""mousedown"The mouse event to listen for.

Hook Source Code

import { RefObject, useEffect, useRef } from "react";

type Handler = (event: MouseEvent | TouchEvent) => void;

/**
 * A hook that detects clicks outside of the specified element and calls the provided handler.
 *
 * @param ref - The ref of the element to detect clicks outside of.
 * @param handler - The function to call when a click outside occurs.
 * @param mouseEvent - The mouse event to listen for. Defaults to "mousedown".
 */
export function useClickOutside<T extends HTMLElement = HTMLElement>(
    ref: RefObject<T | null>,
    handler: Handler,
    mouseEvent: "mousedown" | "mouseup" = "mousedown",
): void {
    const savedHandler = useRef<Handler>(handler);

    // Update the saved handler if it changes, to avoid re-running the effect
    useEffect(() => {
        savedHandler.current = handler;
    }, [handler]);

    useEffect(() => {
        const listener = (event: MouseEvent | TouchEvent) => {
            const el = ref?.current;

            // Do nothing if clicking ref's element or descendent elements
            if (!el || el.contains(event.target as Node)) {
                return;
            }

            savedHandler.current(event);
        };

        document.addEventListener(mouseEvent, listener);
        document.addEventListener("touchstart", listener);

        return () => {
            document.removeEventListener(mouseEvent, listener);
            document.removeEventListener("touchstart", listener);
        };
    }, [ref, mouseEvent]);
}