Fiber UI LogoFiberUI

useComboKeyPress

A React hook that detects complex keyboard shortcuts with modifier keys (Ctrl, Shift, Alt, Meta).

Installation

npx shadcn@latest add https://r.fiberui.com/r/hooks/use-combo-key-press.json

Features

  • Complex Combos - Detects combinations like Ctrl+Shift+S.
  • Modifier Support - First-class support for ctrl, shift, alt, and meta (Cmd).
  • Callback Mode - Executes a function when the combo is pressed.
  • State Mode - Returns a boolean indicating if the combo is currently active.
  • Cross-Platform - Handles ctrl vs meta key differences gracefully.

Source Code

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

Interactive Examples

Basic Shortcut (Ctrl + S)

A common pattern for triggering actions like saving.

Save Shortcut (Ctrl/Cmd + S)

Press Ctrl + S to trigger save.

"use client";

import { useComboKeyPress } from "@repo/hooks/dom/use-combo-key-press";
import { Save } from "lucide-react";
import { useState } from "react";

export const Example1 = () => {
    const [saved, setSaved] = useState(false);

    // Detect Ctrl+S (Cmd+S)
    useComboKeyPress({ key: "s", ctrl: true }, () => {
        // e.preventDefault() is handled by default options
        setSaved(true);
        setTimeout(() => setSaved(false), 2000);
    });

    return (
        <div className="flex w-full max-w-md flex-col items-center gap-6">
            <h3 className="font-semibold">Save Shortcut (Ctrl/Cmd + S)</h3>

            <div
                className={`rounded-full p-4 transition-all duration-300 ${
                    saved
                        ? "scale-110 bg-green-100 text-green-600"
                        : "bg-muted text-muted-foreground"
                }`}
            >
                <Save className="h-8 w-8" />
            </div>

            <p className="text-center text-sm">
                Press{" "}
                <kbd className="bg-muted rounded border px-1 py-0.5 text-xs">
                    Ctrl
                </kbd>{" "}
                +{" "}
                <kbd className="bg-muted rounded border px-1 py-0.5 text-xs">
                    S
                </kbd>{" "}
                to trigger save.
            </p>

            {saved && (
                <div className="animate-in fade-in slide-in-from-bottom-2 text-sm font-medium text-green-600">
                    Saved successfully!
                </div>
            )}
        </div>
    );
};
useComboKeyPress({ key: "s", ctrl: true }, () => {
    saveDocument();
});

Select All (Cmd + A)

A common pattern for selecting all items in a list or text area.

Select All Shortcut

  • Item 1
  • Item 2
  • Item 3
  • Item 4

Press Ctrl/Cmd + A to select all items.

"use client";

import { useComboKeyPress } from "@repo/hooks/dom/use-combo-key-press";
import { Check } from "lucide-react";
import { useState } from "react";

export const Example2 = () => {
    const [items, setItems] = useState([
        { id: 1, label: "Item 1", selected: false },
        { id: 2, label: "Item 2", selected: false },
        { id: 3, label: "Item 3", selected: false },
        { id: 4, label: "Item 4", selected: false },
    ]);

    // Detect Ctrl+A (or Cmd+A on Mac)
    useComboKeyPress({ key: "a", ctrl: true }, (e) => {
        e.preventDefault(); // Prevent native text selection
        setItems((prev) => prev.map((item) => ({ ...item, selected: true })));
    });

    const toggleSelection = (id: number) => {
        setItems((prev) =>
            prev.map((item) =>
                item.id === id ? { ...item, selected: !item.selected } : item,
            ),
        );
    };

    return (
        <div className="flex w-full max-w-md flex-col gap-4">
            <h3 className="font-semibold">Select All Shortcut</h3>

            <div className="bg-muted/50 rounded-lg p-2">
                <ul className="space-y-1">
                    {items.map((item) => (
                        <li
                            key={item.id}
                            onClick={() => toggleSelection(item.id)}
                            className={`flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm transition-colors ${
                                item.selected
                                    ? "bg-primary text-primary-foreground"
                                    : "hover:bg-muted-foreground/10"
                            }`}
                        >
                            <span>{item.label}</span>
                            {item.selected && <Check className="h-4 w-4" />}
                        </li>
                    ))}
                </ul>
            </div>

            <p className="text-muted-foreground text-center text-xs">
                Press <kbd className="font-mono">Ctrl/Cmd + A</kbd> to select
                all items.
            </p>
        </div>
    );
};
useComboKeyPress({ key: "a", ctrl: true }, (e) => {
    e.preventDefault();
    selectAllItems();
});

API Reference

Signature

const isPressed = useComboKeyPress(combo, callback?, options?);

Parameters

ParameterTypeDescription
comboKeyComboObject defining the key combination.
callback(event) => voidOptional function to call when combo is matched.
optionsEventListenerOptionsStandard options. Defaults to { preventDefault: true }.

KeyCombo Interface

PropertyTypeDescription
keystringThe main key to listen for (e.g., "s", "Enter").
ctrlbooleanRequire Control (or Command on Mac) key.
shiftbooleanRequire Shift key.
altbooleanRequire Alt (or Option) key.
metabooleanRequire Meta (Command/Windows) key specifically.

Returns

TypeDescription
booleantrue if the full combination is currently pressed.

Hook Source Code

import { useState, useCallback, useRef, useEffect } from "react";
import {
    useEventListener,
    EventListenerOptions,
} from "@repo/hooks/dom/use-event-listener";

interface KeyCombo {
    /** The main key (e.g., "a", "s", "Escape", "Enter") */
    key: string;
    /** Require Ctrl key (Cmd on Mac for cross-platform shortcuts) */
    ctrl?: boolean;
    /** Require Meta key (Cmd on Mac, Win on Windows) */
    meta?: boolean;
    /** Require Shift key */
    shift?: boolean;
    /** Require Alt key (Option on Mac) */
    alt?: boolean;
}

/**
 * useComboKeyPress - Detect keyboard shortcuts with modifier keys
 *
 * @param combo - The key combination to detect
 * @param callback - Optional callback when combo is pressed
 * @param options - Optional event listener options (preventDefault defaults to true)
 * @returns boolean indicating if the combo is currently pressed
 *
 * @example
 * // Using the boolean state
 * const isSavePressed = useComboKeyPress({ key: "s", ctrl: true });
 *
 * // With callback (Ctrl+S or Cmd+S on Mac)
 * useComboKeyPress({ key: "s", ctrl: true }, () => save());
 *
 * // Ctrl+Shift+A
 * const isSelectAllPressed = useComboKeyPress(
 *   { key: "a", ctrl: true, shift: true },
 *   () => selectAll()
 * );
 */
export function useComboKeyPress(
    combo: KeyCombo,
    callback?: (event: KeyboardEvent) => void,
    options: EventListenerOptions = { preventDefault: true },
): boolean {
    const [isPressed, setIsPressed] = useState(false);
    const callbackRef = useRef(callback);

    useEffect(() => {
        callbackRef.current = callback;
    }, [callback]);

    const matchesCombo = useCallback(
        (event: KeyboardEvent): boolean => {
            const keyMatches =
                event.key.localeCompare(combo.key, undefined, {
                    sensitivity: "base",
                }) === 0;

            const ctrlMatches = combo.ctrl
                ? event.ctrlKey || event.metaKey
                : !event.ctrlKey && !event.metaKey;
            const shiftMatches = combo.shift ? event.shiftKey : !event.shiftKey;
            const altMatches = combo.alt ? event.altKey : !event.altKey;

            return keyMatches && ctrlMatches && shiftMatches && altMatches;
        },
        [combo],
    );

    const handleKeyDown = useCallback(
        (event: KeyboardEvent) => {
            if (matchesCombo(event)) {
                setIsPressed(true);
                callbackRef.current?.(event);
            }
        },
        [matchesCombo],
    );

    const handleKeyUp = useCallback(
        (event: KeyboardEvent) => {
            if (matchesCombo(event)) {
                setIsPressed(false);
            }
        },
        [matchesCombo],
    );

    useEventListener("keydown", handleKeyDown, null, options);
    useEventListener("keyup", handleKeyUp);

    return isPressed;
}