Package Exports
- react-touch-outside
- react-touch-outside/package.json
Readme
react-touch-outside
π Ultra-lightweight (1.037KB gzipped) React hook and component for detecting clicks/touches outside of elements. Works seamlessly with React (web) and React Native. Zero dependencies, TypeScript ready, tree-shakeable.
π― Perfect For
- Modals & Overlays: Close modals when clicking outside
- Dropdowns & Menus: Hide dropdowns on outside interaction
- Popovers & Tooltips: Dismiss popovers automatically
- Mobile Apps: Touch outside detection for React Native
- Accessibility: Keyboard and screen reader friendly
- Performance: Minimal bundle impact with maximum functionality
β¨ Features
- π Universal: Works with React and React Native out of the box
- β‘ Performance: Optimized with minimal re-renders and efficient event handling
- π― TypeScript: Full type safety with comprehensive IntelliSense support
- π¦ Tree-shakeable: Import only what you need, minimal bundle impact
- π§ Configurable: Flexible options for different use cases and platforms
- π Modern: Built with 2025 best practices and latest React patterns
- π§ͺ Well-tested: Comprehensive test coverage with Vitest
- π± Mobile-friendly: Optimized touch handling for mobile devices
π¦ Installation
npm install react-touch-outside
# or
yarn add react-touch-outside
# or
pnpm add react-touch-outsideπ Requirements
- React 16.8+ (hooks support)
- React Native 0.60+ (for React Native usage)
- TypeScript 4.5+ (for TypeScript support)
- Node.js 16+ (for development)
π Migration from Other Libraries
Replacing other click-outside libraries? It's easy:
// Before (react-click-outside)
import { useClickOutside } from 'react-click-outside'
// After (react-touch-outside) - Same API!
import { useTouchOutside } from 'react-touch-outside'π Quick Start
Hook Usage (Recommended)
import { useTouchOutside } from 'react-touch-outside'
function MyModal() {
const { ref, isOutside } = useTouchOutside((event) => {
console.log('Clicked outside the modal!')
// Close modal, hide dropdown, etc.
})
return (
<div ref={ref} className="modal">
<h2>Modal Content</h2>
<p>Click outside to close</p>
{isOutside && <span>Outside clicked!</span>}
</div>
)
}Component Usage
import { TouchOutside } from 'react-touch-outside'
function Dropdown() {
const [isOpen, setIsOpen] = useState(false)
return (
<TouchOutside
onOutside={() => setIsOpen(false)}
className="dropdown-wrapper"
>
<button onClick={() => setIsOpen(!isOpen)}>
Toggle Dropdown
</button>
{isOpen && (
<div className="dropdown-menu">
<a href="#">Option 1</a>
<a href="#">Option 2</a>
</div>
)}
</TouchOutside>
)
}π API Reference
useTouchOutside(callback, options?)
The main hook for detecting outside interactions.
Parameters
callback(event: Event) => void- Function called when outside interaction is detectedoptionsTouchOutsideOptions- Configuration options (optional)
Returns
{
ref: RefObject<HTMLElement> // Ref to attach to your element
isInside: boolean // Whether last interaction was inside
isOutside: boolean // Whether last interaction was outside
}Options
interface TouchOutsideOptions {
enabled?: boolean // Whether the hook is active (default: true)
eventType?: 'click' | 'touchstart' | 'mousedown' // Event to listen for
capture?: boolean // Use capture phase (default: false)
stopPropagation?: boolean // Stop event propagation (default: false)
onOutside?: (event: Event) => void // Called when outside is detected
onInside?: (event: Event) => void // Called when inside is detected
debounceMs?: number // Debounce delay in milliseconds (default: 0)
}TouchOutside Component
A declarative wrapper component for outside detection.
Props
interface TouchOutsideProps extends TouchOutsideOptions {
children: React.ReactNode // Content to wrap
className?: string // CSS class name
style?: React.CSSProperties // Inline styles
as?: keyof JSX.IntrinsicElements // HTML element to render (default: 'div')
wrapperProps?: Record<string, any> // Additional props for wrapper
}π Advanced Examples
Modal with Escape Key
function AdvancedModal({ isOpen, onClose }) {
const { ref } = useTouchOutside(() => onClose(), {
enabled: isOpen,
stopPropagation: true
})
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') onClose()
}
if (isOpen) {
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}
}, [isOpen, onClose])
if (!isOpen) return null
return (
<div className="modal-overlay">
<div ref={ref} className="modal">
<button onClick={onClose} className="close-btn">Γ</button>
<h2>Advanced Modal</h2>
<p>Click outside or press Escape to close</p>
</div>
</div>
)
}Multi-level Dropdown
function MultiLevelDropdown() {
const [activeLevel, setActiveLevel] = useState(null)
const { ref } = useTouchOutside(() => {
setActiveLevel(null)
}, {
debounceMs: 100 // Prevent rapid toggles
})
return (
<TouchOutside
ref={ref}
onOutside={() => setActiveLevel(null)}
className="dropdown-container"
>
<div className="dropdown-level-1">
<button onClick={() => setActiveLevel(1)}>
Level 1
</button>
{activeLevel === 1 && (
<div className="dropdown-level-2">
<button onClick={() => setActiveLevel(2)}>
Level 2
</button>
{activeLevel === 2 && (
<div className="dropdown-content">
<a href="#">Option A</a>
<a href="#">Option B</a>
</div>
)}
</div>
)}
</div>
</TouchOutside>
)
}React Native Integration
import { useTouchOutside } from 'react-touch-outside'
function MobileModal() {
const { ref, isOutside } = useTouchOutside((event) => {
// Handle outside touch on mobile
console.log('Touched outside modal')
}, {
eventType: 'touchstart' // Use touch events for React Native
})
return (
<View ref={ref} style={styles.modal}>
<Text>Modal Content</Text>
{isOutside && <Text>Outside touched!</Text>}
</View>
)
}Performance Optimized List
function VirtualizedList() {
const { ref } = useTouchOutside(() => {
// Close dropdown when scrolling outside
}, {
debounceMs: 50, // Reduce event frequency
capture: true // Capture during capture phase for better performance
})
return (
<div ref={ref} className="virtual-list">
{/* Virtual list content */}
</div>
)
}π― Best Practices
1. Use the Hook for Custom Logic
// β
Good: Use hook for complex interactions
const { ref } = useTouchOutside((event) => {
if (event.target.closest('.keep-open')) return
onClose()
})
// β Avoid: Over-engineering simple cases
<TouchOutside onOutside={onClose}>
<ComplexComponent />
</TouchOutside>2. Optimize Event Handling
// β
Good: Use debouncing for performance
const { ref } = useTouchOutside(onClose, {
debounceMs: 100
})
// β
Good: Disable when not needed
const { ref } = useTouchOutside(onClose, {
enabled: isOpen
})3. Handle Edge Cases
// β
Good: Check for valid targets
const { ref } = useTouchOutside((event) => {
if (!event.target || !document.contains(event.target)) return
onClose()
})4. Accessibility Considerations
// β
Good: Combine with keyboard navigation
const { ref } = useTouchOutside(onClose)
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}, [onClose])π§ Configuration
Environment Detection
The package automatically detects your environment:
- Web: Uses
clickandtouchstartevents - React Native: Uses
touchstartevents - Mobile Web: Handles both click and touch events
Custom Event Types
// For web applications
const { ref } = useTouchOutside(callback, {
eventType: 'mousedown' // More responsive than 'click'
})
// For React Native
const { ref } = useTouchOutside(callback, {
eventType: 'touchstart'
})π Bundle Size
This package is optimized for minimal bundle impact:
- ESM Gzipped: 1.037KB
- CJS Gzipped: 1.124KB
- ESM Minified: 2.312KB
- CJS Minified: 2.521KB
- TypeScript Definitions: 5.794KB
- Zero Runtime Dependencies: Only React/React Native peer dependencies
- Tree-shakeable: Import only what you need
// Import only the hook (smallest bundle)
import { useTouchOutside } from 'react-touch-outside'
// Import everything (still small!)
import { useTouchOutside, TouchOutside } from 'react-touch-outside'π§ͺ Testing
The package includes comprehensive tests. For testing your components:
import { render, fireEvent } from '@testing-library/react'
import { useTouchOutside } from 'react-touch-outside'
test('should detect outside clicks', () => {
const onOutside = jest.fn()
const { container } = render(
<div>
<div data-testid="inside">Inside</div>
<div data-testid="outside">Outside</div>
</div>
)
fireEvent.click(container.querySelector('[data-testid="outside"]'))
expect(onOutside).toHaveBeenCalled()
})π€ Contributing
Contributions are welcome! Please read our Contributing Guide for details.
Development Setup
git clone https://github.com/ytahirkose/react-touch-outside.git
cd react-touch-outside
npm install
npm run devScripts
npm run dev- Start development servernpm run build- Build for productionnpm run test- Run testsnpm run test:coverage- Run tests with coveragenpm run type-check- TypeScript type checkingnpm run lint- ESLint checking
π License
MIT Β© YaΕar Tahir KΓΆse
π Acknowledgments
- Built with modern React patterns and 2025 best practices
- Inspired by the need for a universal, performant outside click detection solution
- Thanks to the React and React Native communities for their excellent tooling
π Common Use Cases
E-commerce Applications
// Shopping cart popover
const { ref } = useTouchOutside(() => setCartOpen(false))
// Product quick view modal
const { ref } = useTouchOutside(() => setQuickViewOpen(false))Dashboard Applications
// User profile dropdown
const { ref } = useTouchOutside(() => setProfileOpen(false))
// Settings panel
const { ref } = useTouchOutside(() => setSettingsOpen(false))Mobile Applications (React Native)
// Bottom sheet
const { ref } = useTouchOutside(() => setBottomSheetOpen(false))
// Action sheet
const { ref } = useTouchOutside(() => setActionSheetOpen(false))π Comparison with Other Libraries
| Feature | react-touch-outside | react-click-outside | react-outside-click-handler |
|---|---|---|---|
| Bundle Size | 1.037KB gzipped | ~2KB+ | ~3KB+ |
| React Native | β Native support | β Web only | β Web only |
| TypeScript | β Full support | β οΈ Partial | β οΈ Partial |
| Tree Shaking | β Optimized | β Limited | β Limited |
| Zero Dependencies | β Yes | β No | β No |
| Modern API | β Hooks + Components | β οΈ HOC only | β οΈ HOC only |
π Performance Tips
- Use debouncing for high-frequency events:
const { ref } = useTouchOutside(callback, { debounceMs: 100 })- Disable when not needed:
const { ref } = useTouchOutside(callback, { enabled: isOpen })- Use capture phase for better performance:
const { ref } = useTouchOutside(callback, { capture: true })π Bundle Analysis
Want to see the exact impact on your bundle? Check out Bundlephobia for detailed analysis.
π€ Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Commands
npm run dev # Start development server
npm run build # Build for production
npm run test # Run tests
npm run lint # Run ESLint
npm run type-check # TypeScript type checkingπ License
MIT Β© YaΕar Tahir KΓΆse
π Acknowledgments
- Built with modern React patterns and 2025 best practices
- Inspired by the need for a universal, performant outside click detection solution
- Thanks to the React and React Native communities for their excellent tooling
- Special thanks to all contributors and users
Made with β€οΈ for the React community
β Star this repo | π Report an issue | π‘ Request a feature | π Documentation