elements
lifecycle
browser
- useAudio
- useBattery
- useBluetooth
- useBreakpoints
- useBroadcastChannel
- useBrowserLocation
- useClipboard
- useCopy
- useCssVar
- useDisplayMedia
- useDocumentEvent
- useDocumentTitle
- useDocumentVisibility
- useEventListener
- useEventSource
- useEyeDropper
- useFavicon
- useFileSystemAccess
- useFps
- useFullscreen
- useGamepad
- useGeolocation
- useMeasure
- useMediaControls
- useMediaQuery
- useMemory
- useNetwork
- useObjectUrl
- useOnline
- useOtpCredential
- usePermission
- usePictureInPicture
- usePointerLock
- usePostMessage
- useRaf
- useShare
- useSpeechRecognition
- useSpeechSynthesis
- useSticky
- useVibrate
- useVirtualKeyboard
- useWakeLock
- useWebSocket
utilities
state
- useBoolean
- useControllableState
- useCookie
- useCookies
- useCounter
- useCycleList
- useDefault
- useDisclosure
- useField
- useHash
- useList
- useLocalStorage
- useMap
- useMask
- useMergedRef
- useObject
- useOffsetPagination
- useQueue
- useRafState
- useRefState
- useSessionStorage
- useSet
- useStateHistory
- useStep
- useStorage
- useToggle
- useUrlSearchParam
- useUrlSearchParams
- useValidatedState
- useWizard
user
sensors
- useDeviceMotion
- useDeviceOrientation
- useHotkeys
- useIdle
- useInfiniteScroll
- useIntersectionObserver
- useKeyboard
- useKeyPress
- useKeysPressed
- useMouse
- useMutationObserver
- useOrientation
- usePageLeave
- useParallax
- usePerformanceObserver
- useResizeObserver
- useScroll
- useScrollIntoView
- useScrollTo
- useSwipe
- useTextSelection
- useVisibility
- useWindowEvent
- useWindowFocus
- useWindowScroll
- useWindowSize
Open on a mobile device to see the bubble move.
import { useDeviceMotion } from '@siberiacancode/reactuse';
import { SmartphoneIcon } from 'lucide-react';
const CIRCLE_SIZE = 240;
const BUBBLE_SIZE = 22;
const MAX_OFFSET = CIRCLE_SIZE / 2 - BUBBLE_SIZE / 2 - 12;
const GRAVITY = 9.8;
const LEVEL_THRESHOLD = 2;
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
const Demo = () => {
const deviceMotion = useDeviceMotion();
const value = deviceMotion.watch();
const x = value.accelerationIncludingGravity.x;
const y = value.accelerationIncludingGravity.y;
const hasData = x !== null && y !== null;
const offsetX = hasData ? clamp((-x / GRAVITY) * MAX_OFFSET, -MAX_OFFSET, MAX_OFFSET) : 0;
const offsetY = hasData ? clamp((y / GRAVITY) * MAX_OFFSET, -MAX_OFFSET, MAX_OFFSET) : 0;
const tiltX = hasData ? (-x / GRAVITY) * 90 : 0;
const tiltY = hasData ? (y / GRAVITY) * 90 : 0;
const isLevel = hasData && Math.abs(tiltX) < LEVEL_THRESHOLD && Math.abs(tiltY) < LEVEL_THRESHOLD;
const cx = CIRCLE_SIZE / 2;
const cy = CIRCLE_SIZE / 2;
const r = cx - 8;
const formatTilt = (v: number) => {
const sign = v < 0 ? '-' : '';
return `${sign}${Math.abs(v).toFixed(1)} deg`;
};
return (
<section className='flex flex-col items-center gap-6 p-4'>
<div className='relative' style={{ width: CIRCLE_SIZE, height: CIRCLE_SIZE }}>
<svg height={CIRCLE_SIZE} viewBox={`0 0 ${CIRCLE_SIZE} ${CIRCLE_SIZE}`} width={CIRCLE_SIZE}>
<g className='text-border' stroke='currentColor' strokeLinecap='round' strokeWidth='1'>
<line x1={cx} x2={cx} y1={0} y2={CIRCLE_SIZE} />
<line x1={0} x2={CIRCLE_SIZE} y1={cy} y2={cy} />
</g>
<circle
className='text-border'
cx={cx}
cy={cy}
fill='transparent'
r={r}
stroke='currentColor'
strokeWidth='1.5'
/>
{hasData && (
<circle
className={isLevel ? 'fill-green-500' : 'fill-foreground'}
cx={cx + offsetX}
cy={cy + offsetY}
r={BUBBLE_SIZE / 2}
style={{ transition: 'all 120ms ease-out, fill 200ms ease-out' }}
/>
)}
</svg>
{isLevel && (
<div className='pointer-events-none absolute inset-x-0 -bottom-8 text-center'>
<span className='font-mono text-xs font-semibold tracking-[0.2em] text-green-500'>
LEVEL
</span>
</div>
)}
{!hasData && (
<div className='absolute inset-0 flex flex-col items-center justify-center gap-2 px-10 text-center'>
<SmartphoneIcon className='text-muted-foreground size-8' />
<p className='text-muted-foreground text-xs'>
Open on a mobile device to see the bubble move.
</p>
</div>
)}
</div>
{hasData && (
<div className='text-foreground flex items-center gap-3 pt-4 font-mono text-sm tracking-wider tabular-nums'>
<span>X {formatTilt(tiltX)}</span>
<span className='text-muted-foreground'>|</span>
<span>Y {formatTilt(tiltY)}</span>
</div>
)}
</section>
);
};
export default Demo;
This hook uses DeviceMotionEvent browser api to provide enhanced functionality. Make sure to check for compatibility with different browsers when using this api
Installation
pnpm add @siberiacancode/reactuseUsage
const { interval, rotationRate, acceleration, accelerationIncludingGravity } = useDeviceMotion((event) => console.log(event)).watch();
// or
const { interval, rotationRate, acceleration, accelerationIncludingGravity } = useDeviceMotion().watch();Type Declarations
export interface UseDeviceMotionReturn {
snapshot: UseDeviceMotionValue;
watch: () => UseDeviceMotionValue;
}
export interface UseDeviceMotionValue {
acceleration: DeviceMotionEventAcceleration;
accelerationIncludingGravity: DeviceMotionEventAcceleration;
interval: DeviceMotionEvent['interval'];
rotationRate: DeviceMotionEventRotationRate;
}
export interface UseDeviceMotionOptions {
/** Whether to enable the hook */
enabled?: boolean;
/** The callback function to be invoked */
onChange?: (event: DeviceMotionEvent) => void;
}
export interface UseDeviceMotion {
(callback?: (event: DeviceMotionEvent) => void): UseDeviceMotionReturn;
(options?: UseDeviceMotionOptions): UseDeviceMotionReturn;
}API
Parameters
| Name | Type | Default | Note |
|---|---|---|---|
| callback | (event: DeviceMotionEvent) => void | - | The callback function to be invoked |
Returns
UseDeviceMotionReturnParameters
| Name | Type | Default | Note |
|---|---|---|---|
| options | UseDeviceMotionOptions | - | Configuration options |
| options.enabled | boolean | - | Whether to enable the hook |
| options.onChange | (event: DeviceMotionEvent) => void | - | The callback function to be invoked |
Returns
UseDeviceMotionReturnContributors
ddebabinMMichael Mironychev
Last updated on