useHooks.iov4.1.2
DocsBlogGitHub
Hooks
No hooks found in any category.

useDeviceOrientation

sensors

Installation

npx usehooks-cli@latest add use-device-orientation

Description

A hook for accessing device orientation data using the DeviceOrientationEvent API. Provides alpha, beta, and gamma rotation values along with compass heading on supported devices.

Parameters

NameTypeDefaultDescription
options?UseDeviceOrientationOptions-Configuration options for device orientation

Parameter Properties

options properties:

NameTypeDescription
absolute?booleanWhether to request absolute orientation values (auto-starts listening)

Return Type

UseDeviceOrientationReturn
PropertyTypeDescription
orientationDeviceOrientation | nullCurrent device orientation data including alpha, beta, gamma values
isSupportedbooleanWhether DeviceOrientationEvent is supported
errorstring | nullError message if orientation access failed
requestPermission() => Promise<boolean>Request permission for device orientation (required on iOS 13+)
startListening() => voidStart listening to orientation changes
stopListening() => voidStop listening to orientation changes
isListeningbooleanWhether currently listening to orientation changes

Examples

Basic Orientation Display

Display device orientation values with manual start/stop

1import { useDeviceOrientation } from '@usehooks/use-device-orientation'; 2 3function OrientationDisplay() { 4 const { 5 orientation, 6 isSupported, 7 isListening, 8 error, 9 requestPermission, 10 startListening, 11 stopListening 12 } = useDeviceOrientation(); 13 14 const handleStart = async () => { 15 const hasPermission = await requestPermission(); 16 if (hasPermission) { 17 startListening(); 18 } 19 }; 20 21 if (!isSupported) { 22 return <div>Device orientation not supported</div>; 23 } 24 25 return ( 26 <div> 27 <div> 28 <button onClick={handleStart} disabled={isListening}> 29 Start Listening 30 </button> 31 <button onClick={stopListening} disabled={!isListening}> 32 Stop Listening 33 </button> 34 </div> 35 36 <p>Status: {isListening ? 'Listening' : 'Stopped'}</p> 37 38 {error && <p>Error: {error}</p>} 39 40 {orientation && ( 41 <div> 42 <h3>Device Orientation:</h3> 43 <p>Alpha (Z-axis): {orientation.alpha?.toFixed(2)}°</p> 44 <p>Beta (X-axis): {orientation.beta?.toFixed(2)}°</p> 45 <p>Gamma (Y-axis): {orientation.gamma?.toFixed(2)}°</p> 46 <p>Absolute: {orientation.absolute ? 'Yes' : 'No'}</p> 47 {orientation.webkitCompassHeading && ( 48 <p>Compass: {orientation.webkitCompassHeading.toFixed(2)}°</p> 49 )} 50 </div> 51 )} 52 </div> 53 ); 54}

Compass Application

Create a simple compass using device orientation

1import { useDeviceOrientation } from '@usehooks/use-device-orientation'; 2import { useEffect, useState } from 'react'; 3 4function Compass() { 5 const { orientation, requestPermission, startListening } = useDeviceOrientation(); 6 const [compassHeading, setCompassHeading] = useState(0); 7 8 useEffect(() => { 9 const initCompass = async () => { 10 const hasPermission = await requestPermission(); 11 if (hasPermission) { 12 startListening(); 13 } 14 }; 15 initCompass(); 16 }, []); 17 18 useEffect(() => { 19 if (orientation) { 20 // Use webkitCompassHeading if available, otherwise use alpha 21 const heading = orientation.webkitCompassHeading ?? orientation.alpha ?? 0; 22 setCompassHeading(heading); 23 } 24 }, [orientation]); 25 26 const compassStyle = { 27 width: '200px', 28 height: '200px', 29 border: '2px solid #333', 30 borderRadius: '50%', 31 position: 'relative' as const, 32 margin: '20px auto', 33 backgroundColor: '#f0f0f0' 34 }; 35 36 const needleStyle = { 37 position: 'absolute' as const, 38 top: '10px', 39 left: '50%', 40 width: '2px', 41 height: '80px', 42 backgroundColor: 'red', 43 transformOrigin: 'bottom center', 44 transform: `translateX(-50%) rotate(${compassHeading}deg)`, 45 transition: 'transform 0.3s ease' 46 }; 47 48 return ( 49 <div style={{ textAlign: 'center' }}> 50 <h2>Compass</h2> 51 <div style={compassStyle}> 52 <div style={needleStyle}></div> 53 <div style={{ position: 'absolute', top: '5px', left: '50%', transform: 'translateX(-50%)' }}>N</div> 54 <div style={{ position: 'absolute', bottom: '5px', left: '50%', transform: 'translateX(-50%)' }}>S</div> 55 <div style={{ position: 'absolute', left: '5px', top: '50%', transform: 'translateY(-50%)' }}>W</div> 56 <div style={{ position: 'absolute', right: '5px', top: '50%', transform: 'translateY(-50%)' }}>E</div> 57 </div> 58 <p>Heading: {compassHeading.toFixed(1)}°</p> 59 </div> 60 ); 61}

Auto-start with Absolute Values

Automatically start listening with absolute orientation values

1import { useDeviceOrientation } from '@usehooks/use-device-orientation'; 2 3function AbsoluteOrientation() { 4 const { 5 orientation, 6 isSupported, 7 isListening, 8 error 9 } = useDeviceOrientation({ absolute: true }); 10 11 if (!isSupported) { 12 return <div>Device orientation not supported</div>; 13 } 14 15 return ( 16 <div> 17 <h3>Absolute Device Orientation</h3> 18 <p>Status: {isListening ? 'Active' : 'Inactive'}</p> 19 20 {error && ( 21 <div style={{ color: 'red' }}> 22 <p>Error: {error}</p> 23 <p>Note: On iOS, you may need to enable motion access in Settings.</p> 24 </div> 25 )} 26 27 {orientation && ( 28 <div style={{ fontFamily: 'monospace' }}> 29 <div>Alpha (Z): {orientation.alpha?.toFixed(3)}°</div> 30 <div>Beta (X): {orientation.beta?.toFixed(3)}°</div> 31 <div>Gamma (Y): {orientation.gamma?.toFixed(3)}°</div> 32 <div>Absolute: {orientation.absolute ? '✓' : '✗'}</div> 33 34 {/* Visual representation */} 35 <div style={{ marginTop: '20px' }}> 36 <div>Tilt Forward/Back: {orientation.beta ? (orientation.beta > 0 ? 'Forward' : 'Back') : 'Level'}</div> 37 <div>Tilt Left/Right: {orientation.gamma ? (orientation.gamma > 0 ? 'Right' : 'Left') : 'Level'}</div> 38 </div> 39 </div> 40 )} 41 </div> 42 ); 43}

Dependencies

react

Notes

  • Requires user permission on iOS 13+ devices
  • HTTPS required in production environments
  • Alpha represents rotation around Z-axis (0-360°)
  • Beta represents rotation around X-axis (-180° to 180°)
  • Gamma represents rotation around Y-axis (-90° to 90°)
  • WebKit compass heading available on some iOS devices
  • Automatically cleans up event listeners on unmount

Implementation

1'use client'; 2 3import { useState, useEffect, useCallback } from "react"; 4 5interface DeviceOrientation { 6 alpha: number | null; 7 beta: number | null; 8 gamma: number | null; 9 absolute: boolean; 10 webkitCompassHeading?: number; 11 webkitCompassAccuracy?: number; 12} 13 14interface UseDeviceOrientationOptions { 15 absolute?: boolean; 16} 17 18interface UseDeviceOrientationReturn { 19 orientation: DeviceOrientation | null; 20 isSupported: boolean; 21 error: string | null; 22 requestPermission: () => Promise<boolean>; 23 startListening: () => void; 24 stopListening: () => void; 25 isListening: boolean; 26} 27 28// Extend DeviceOrientationEvent interface for webkit properties 29declare global { 30 interface DeviceOrientationEvent { 31 webkitCompassHeading?: number; 32 webkitCompassAccuracy?: number; 33 } 34} 35 36export const useDeviceOrientation = ( 37 options: UseDeviceOrientationOptions = {} 38): UseDeviceOrientationReturn => { 39 const [orientation, setOrientation] = useState<DeviceOrientation | null>( 40 null 41 ); 42 const [isListening, setIsListening] = useState(false); 43 const [error, setError] = useState<string | null>(null); 44 45 // Check if DeviceOrientationEvent is supported 46 const isSupported = 47 typeof window !== "undefined" && "DeviceOrientationEvent" in window; 48 49 // Handle orientation change 50 const handleOrientationChange = useCallback( 51 (event: DeviceOrientationEvent) => { 52 setOrientation({ 53 alpha: event.alpha, 54 beta: event.beta, 55 gamma: event.gamma, 56 absolute: event.absolute, 57 webkitCompassHeading: event.webkitCompassHeading, 58 webkitCompassAccuracy: event.webkitCompassAccuracy, 59 }); 60 }, 61 [] 62 ); 63 64 // Request permission for iOS 13+ devices 65 const requestPermission = useCallback(async (): Promise<boolean> => { 66 if (!isSupported) { 67 setError("DeviceOrientationEvent is not supported"); 68 return false; 69 } 70 71 try { 72 setError(null); 73 74 // Check if permission is required (iOS 13+) 75 if ( 76 typeof (DeviceOrientationEvent as any).requestPermission === "function" 77 ) { 78 const permission = await ( 79 DeviceOrientationEvent as any 80 ).requestPermission(); 81 82 if (permission === "granted") { 83 return true; 84 } else { 85 setError("Permission denied for device orientation"); 86 return false; 87 } 88 } 89 90 // Permission not required or already granted 91 return true; 92 } catch (err) { 93 const errorMessage = 94 err instanceof Error ? err.message : "Failed to request permission"; 95 setError(errorMessage); 96 return false; 97 } 98 }, [isSupported]); 99 100 // Start listening to orientation changes 101 const startListening = useCallback(() => { 102 if (!isSupported || isListening) return; 103 104 try { 105 setError(null); 106 window.addEventListener( 107 "deviceorientation", 108 handleOrientationChange, 109 true 110 ); 111 setIsListening(true); 112 } catch (err) { 113 const errorMessage = 114 err instanceof Error ? err.message : "Failed to start listening"; 115 setError(errorMessage); 116 } 117 }, [isSupported, isListening, handleOrientationChange]); 118 119 // Stop listening to orientation changes 120 const stopListening = useCallback(() => { 121 if (!isSupported || !isListening) return; 122 123 try { 124 window.removeEventListener( 125 "deviceorientation", 126 handleOrientationChange, 127 true 128 ); 129 setIsListening(false); 130 setOrientation(null); 131 } catch (err) { 132 const errorMessage = 133 err instanceof Error ? err.message : "Failed to stop listening"; 134 setError(errorMessage); 135 } 136 }, [isSupported, isListening, handleOrientationChange]); 137 138 // Auto-start listening if absolute option is provided 139 useEffect(() => { 140 if (options.absolute !== undefined && isSupported) { 141 const initializeOrientation = async () => { 142 const hasPermission = await requestPermission(); 143 if (hasPermission) { 144 startListening(); 145 } 146 }; 147 148 initializeOrientation(); 149 } 150 151 return () => { 152 if (isListening) { 153 stopListening(); 154 } 155 }; 156 }, [options.absolute, isSupported]); 157 158 // Cleanup on unmount 159 useEffect(() => { 160 return () => { 161 if (isListening) { 162 stopListening(); 163 } 164 }; 165 }, [isListening, stopListening]); 166 167 return { 168 orientation, 169 isSupported, 170 error, 171 requestPermission, 172 startListening, 173 stopListening, 174 isListening, 175 }; 176}; 177