useAutoFocusedInput
Auto-focus an input on typing non-modifier keys. Supports Escape/Enter blur with optional callbacks.
useAutoFocusedInput
automatically focuses an input when the user types a non-modifier key anywhere on the page. It also supports blurring the input on Escape
/Enter
and optional callbacks for those events. This provides a keyboard-first UX that feels snappy without requiring explicit focusing logic.
Code
import { useCallback, useEffect, useRef } from 'react';
export interface IUseAutoFocusedInputProps {
onEscape?: () => void;
onEnter?: () => void;
shouldFocus?: (e: KeyboardEvent) => boolean;
disabled?: boolean;
}
const SPECIAL_KEYS = [
'Tab',
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'Home',
'End',
'Enter',
'PageUp',
'PageDown',
'Shift',
'Control',
'Alt',
'Meta',
'CapsLock',
'AltGraph',
'NumLock',
'ScrollLock',
'ContextMenu',
'Insert',
'Delete',
'Escape',
'Pause',
'PrintScreen',
'Compose',
'Dead',
];
const BLUR_KEYS = ['Escape', 'Enter'];
function isEditableTarget(target: EventTarget | null) {
const el = target as HTMLElement | null;
if (!el) return false;
const tagName = el.tagName?.toLowerCase();
if (tagName === 'input' || tagName === 'textarea') return true;
if (el.isContentEditable) return true;
if (el.getAttribute('role') === 'textbox') return true;
return false;
}
/**
* Automatically focuses a referenced input when the user types, with sensible
* accessibility guards. It ignores navigation/function keys, modifier combos,
* composition events, and will not steal focus from existing text inputs.
*/
export function useAutoFocusedInput<
T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement,
>({
onEscape,
onEnter,
shouldFocus,
disabled,
}: IUseAutoFocusedInputProps = {}) {
const inputRef = useRef<T | null>(null);
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
if (disabled || e.isComposing) return;
if (e.metaKey || e.ctrlKey || e.altKey) return;
// Do not hijack focus when the user is already editing text
if (isEditableTarget(e.target) && e.target !== inputRef.current) {
return;
}
// If the key is in the blurKeys array and the input is currently focused, blur the input
if (BLUR_KEYS.includes(e.key) && e.target === inputRef.current) {
inputRef.current?.blur();
// Call the appropriate callback after blurring
switch (e.key) {
case 'Escape':
onEscape?.();
break;
case 'Enter':
onEnter?.();
break;
}
return;
}
// Function keys: F1-F24 and other special keys
if (SPECIAL_KEYS.includes(e.key) || /^F\d{1,2}$/.test(e.key)) return;
if (shouldFocus && !shouldFocus(e)) return;
const element = inputRef.current;
if (element) {
if (element.disabled || element.readOnly) return;
element.focus();
}
},
[onEscape, onEnter, shouldFocus, disabled],
);
useEffect(() => {
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [onKeyDown]);
return inputRef;
}
Example
Start typing anywhere on the page to auto-focus the input
Press Escape or Enter to blur
Usage
'use client';
import React, { useCallback } from 'react';
import { toast } from 'sonner';
import { Input } from '@/components/intuitive-ui/(native)/input';
import { useAutoFocusedInput } from '@/hooks/use-auto-focused-input';
import Container from '@/app/repository/[topic]/[slug]/_components/container';
const BasicExample: React.FC = () => {
const onEscape = useCallback(() => toast('Escape pressed'), []);
const onEnter = useCallback(() => toast('Enter pressed'), []);
const inputRef = useAutoFocusedInput({ onEscape, onEnter });
return (
<div className="flex flex-col gap-4">
<Container>
<p className="text-muted-foreground text-sm">
Start typing anywhere on the page to auto-focus the input
</p>
<Input
ref={inputRef}
placeholder="Start typing to focus me"
className="max-w-sm"
/>
<p className="text-muted-foreground text-sm">
Press Escape or Enter to blur
</p>
</Container>
</div>
);
};
export default BasicExample;