useNormalizedKeys
A React hook for normalized keyboard input handling, designed for productivity applications, drawing tools, and professional interfaces with advanced features like sequence detection, tap/hold recognition, and cross-platform compatibility.
What is useNormalizedKeys?
useNormalizedKeys is a comprehensive React hook that provides consistent, feature-rich keyboard input handling across different browsers and platforms. It's designed for applications that require precise, real-time keyboard interaction like text editors, drawing applications, design tools, code editors, and professional productivity applications.
Key Features
- ⚡ Professional Shortcuts - Build keyboard-driven interfaces like Photoshop, Figma, VS Code
- 🚀 Smooth Animations - RequestAnimationFrame for perfectly fluid visual feedback
- 🔄 Context Provider - Simplified setup with automatic state management
- 🌐 Cross-platform compatibility - Handles Windows, macOS, and Linux keyboard differences seamlessly
- 🎨 Drawing Tools Ready - Optimized for creative applications with pressure sensitivity and tool switching
- 🎹 Advanced Sequences - Detect complex shortcuts (Ctrl+S), key sequences (jk), and chord combinations
- ⏱️ Tap vs Hold detection - Distinguish between quick taps and long holds for different actions
- 🚫 Smart Prevention - Block browser shortcuts selectively while respecting input fields
- 🔤 Key normalization - Consistent key names across different browsers and layouts
- 📊 Rich event data - Detailed information about timing, modifiers, accessibility state, and more
- 📝 TypeScript First - Complete type definitions with excellent IntelliSense support
- 🔧 Zero configuration - Works out of the box with sensible defaults for professional use
Quick Example
import { useNormalizedKeys, Keys } from 'use-normalized-keys';
function TextEditor() {
const keys = useNormalizedKeys({
sequences: [
{ id: 'vim-escape', keys: [Keys.j, Keys.k], type: 'sequence' },
{ id: 'save', keys: [Keys.CONTROL, Keys.s], type: 'chord' }
],
onSequenceMatch: (match) => {
if (match.sequenceId === 'save') console.log('Document saved!');
if (match.sequenceId === 'vim-escape') console.log('Exit insert mode');
},
preventDefault: true, // Prevent browser shortcuts like Ctrl+S
tapHoldThreshold: 150 // 150ms for tap vs hold
});
return (
<div>
<textarea placeholder="Start typing..." />
<p>Last key: {keys.lastEvent?.key}
{keys.lastEvent?.isTap && ' (tap)'}
{keys.lastEvent?.isHold && ' (hold)'}
</p>
<div>
<h3>Productivity Shortcuts</h3>
<ul>
<li>Save (Ctrl+S): {keys.isKeyPressed([Keys.CONTROL, Keys.s]) ? '🟢' : '⚪'}</li>
<li>Undo (Ctrl+Z): {keys.isKeyPressed([Keys.CONTROL, Keys.z]) ? '🟢' : '⚪'}</li>
<li>Copy (Ctrl+C): {keys.isKeyPressed([Keys.CONTROL, Keys.c]) ? '🟢' : '⚪'}</li>
<li>Paste (Ctrl+V): {keys.isKeyPressed([Keys.CONTROL, Keys.v]) ? '🟢' : '⚪'}</li>
</ul>
</div>
<div>
<h3>Modifiers</h3>
<p>Shift: {keys.activeModifiers.shift ? '🟢' : '⚪'}</p>
<p>Ctrl: {keys.activeModifiers.ctrl ? '🟢' : '⚪'}</p>
</div>
{keys.sequences?.matches.length > 0 && (
<div>
<h3>Shortcuts Detected</h3>
{keys.sequences.matches.map(match => (
<p key={match.sequenceId}>✨ {match.sequenceId}</p>
))}
</div>
)}
</div>
);
}
Normalized Provider
The Context Provider approach provides key info through a common context:
import {
NormalizedKeysProvider,
useHoldSequence,
holdSequence,
chordSequence,
Keys
} from 'use-normalized-keys';
function BrushTool() {
// Single unified hook for brush pressure sensitivity
const brushPressure = useHoldSequence('brush-pressure');
return (
<div className="brush-tool">
<div
className="brush-preview"
style={{
transform: `scale(${1 + brushPressure.progress / 200})`,
opacity: 0.5 + brushPressure.progress / 200,
boxShadow: brushPressure.glow > 0 ? `0 0 ${brushPressure.glow * 10}px #3b82f6` : 'none'
}}
>
<div>Brush Size: {Math.round(10 + brushPressure.progress / 10)}px</div>
<div>Pressure: {Math.round(brushPressure.progress)}%</div>
{brushPressure.isCharging && <div className="charging">Hold Space for pressure</div>}
</div>
</div>
);
}
function App() {
return (
<NormalizedKeysProvider
sequences={[
// Pressure sensitivity for brush
holdSequence('brush-pressure', Keys.SPACE, 100, {
name: 'Brush Pressure',
continuous: true
}),
// Standard shortcuts
chordSequence('save', [Keys.CONTROL, Keys.s], { name: 'Save Project' }),
chordSequence('undo', [Keys.CONTROL, Keys.z], { name: 'Undo' })
]}
>
<BrushTool />
</NormalizedKeysProvider>
);
}
Getting Started
Ready to get started? Check out our installation guide and quick start tutorial.
Or jump right into the live demo to see it in action!