Fiber UI LogoFiberUI

useDeviceOrientation

A comprehensive hook to access the device's physical orientation and tilt angles (alpha, beta, gamma) relative to the Earth's frame of reference.

Installation

npx shadcn@latest add https://r.fiberui.com/r/hooks/use-device-orientation.json

A React hook that provides real-time access to the device's physical orientation using the DeviceOrientation Event. Used for games, parallax effects, or augmented reality.

Browser Support

Checks for isSupported. Use requestPermission for iOS 13+ devices which require user interaction to grant access to motion sensors.

Features

  • Real-time Updates - Tracks alpha, beta, and gamma rotation values
  • IOS Support - Handles permission request flow for iOS 13+
  • Type Safe - Fully typed return values
  • SSR Safe - Works safely with server-side rendering

Basic Usage

Display the current device orientation data. (Try this on a mobile device!)

0°
Alpha (Z)
0°
Beta (X)
0°
Gamma (Y)
"use client";

import { useDeviceOrientation } from "@repo/hooks/device/use-device-orientation";
import { Button } from "@repo/ui/components/button";
import { Smartphone } from "lucide-react";

export function Example1() {
    const { orientation, requestPermission, error } = useDeviceOrientation();

    // Round values for display
    const alpha = orientation.alpha ? Math.round(orientation.alpha) : 0;
    const beta = orientation.beta ? Math.round(orientation.beta) : 0;
    const gamma = orientation.gamma ? Math.round(orientation.gamma) : 0;

    return (
        <div className="flex flex-col gap-4">
            <div className="grid grid-cols-3 gap-4 text-center">
                <div className="rounded-lg border p-4">
                    <div className="text-2xl font-bold">{alpha}°</div>
                    <div className="text-muted-foreground text-xs">
                        Alpha (Z)
                    </div>
                </div>
                <div className="rounded-lg border p-4">
                    <div className="text-2xl font-bold">{beta}°</div>
                    <div className="text-muted-foreground text-xs">
                        Beta (X)
                    </div>
                </div>
                <div className="rounded-lg border p-4">
                    <div className="text-2xl font-bold">{gamma}°</div>
                    <div className="text-muted-foreground text-xs">
                        Gamma (Y)
                    </div>
                </div>
            </div>

            {error && (
                <div className="text-destructive text-sm">{error.message}</div>
            )}

            <Button onClick={requestPermission} variant="outline">
                <Smartphone className="mr-2 h-4 w-4" />
                Request Permission (iOS)
            </Button>
        </div>
    );
}

API Reference

Hook Signature

function useDeviceOrientation(): UseDeviceOrientationReturn;

Return Value

PropertyTypeDescription
orientationDeviceOrientationStateObject containing alpha, beta, gamma values
errorError | nullError object if permission denied or unsupported
requestPermission() => Promise<boolean>Request permission (iOS 13+). Returns true if granted
isSupportedbooleantrue if DeviceOrientation API is available

DeviceOrientationState

interface DeviceOrientationState {
    alpha: number | null; // Z axis (0 to 360)
    beta: number | null; // X axis (-180 to 180) - Front/Back Tilt
    gamma: number | null; // Y axis (-90 to 90) - Left/Right Tilt
    absolute: boolean; // Whether values are absolute (Earth coords)
}

Hook Source Code

import { useState, useCallback, useEffect } from "react";

/**
 * Orientation data return type
 */
export interface DeviceOrientationState {
    /** The correlation of the device's Z axis to the Earth's Z axis (0 to 360)  */
    alpha: number | null;
    /** The inclination value (forward vs backward) (-180 to 180) */
    beta: number | null;
    /** The inclination value (left vs right) (-90 to 90) */
    gamma: number | null;
    /** Whether the data provided is absolute */
    absolute: boolean;
}

/**
 * Return type for the useDeviceOrientation hook
 */
export interface UseDeviceOrientationReturn {
    /** Current orientation state */
    orientation: DeviceOrientationState;
    /** Error state if any */
    error: Error | null;
    /** Request permission (required for iOS 13+) */
    requestPermission: () => Promise<boolean>;
    /** Whether the API is supported */
    isSupported: boolean;
}

// Add iOS-specific types
interface DeviceOrientationEventiOS extends DeviceOrientationEvent {
    requestPermission?: () => Promise<"granted" | "denied">;
}

/**
 * A React hook that provides access to the device's physical orientation.
 * Handles permission requests for iOS 13+ devices.
 *
 * @returns UseDeviceOrientationReturn object with orientation data and controls
 */
export function useDeviceOrientation(): UseDeviceOrientationReturn {
    const [orientation, setOrientation] = useState<DeviceOrientationState>({
        alpha: null,
        beta: null,
        gamma: null,
        absolute: false,
    });
    const [error, setError] = useState<Error | null>(null);

    // Check support
    const isSupported =
        typeof window !== "undefined" && "DeviceOrientationEvent" in window;

    // Handle orientation change
    const handleOrientation = useCallback((event: DeviceOrientationEvent) => {
        setOrientation({
            alpha: event.alpha,
            beta: event.beta,
            gamma: event.gamma,
            absolute: event.absolute,
        });
    }, []);

    // Request permission (iOS 13+)
    const requestPermission = useCallback(async (): Promise<boolean> => {
        if (!isSupported) {
            setError(new Error("DeviceOrientationEvent is not supported"));
            return false;
        }

        try {
            // Check for iOS 13+ permission API
            const EventCast =
                DeviceOrientationEvent as unknown as DeviceOrientationEventiOS;

            if (typeof EventCast.requestPermission === "function") {
                const state = await EventCast.requestPermission();
                if (state === "granted") {
                    window.addEventListener(
                        "deviceorientation",
                        handleOrientation,
                    );
                    return true;
                } else {
                    setError(new Error("Permission denied"));
                    return false;
                }
            }

            // Non-iOS 13+ devices don't need permission
            // Or if permission was already granted previously
            return true;
        } catch (err) {
            const error =
                err instanceof Error
                    ? err
                    : new Error("Failed to request permission");
            setError(error);
            return false;
        }
    }, [isSupported, handleOrientation]);

    useEffect(() => {
        if (!isSupported) return;

        // On mount, try to add listener (works if no permission needed or already granted)
        // For iOS 13+, this might do nothing until requestPermission is called
        window.addEventListener("deviceorientation", handleOrientation);

        return () => {
            window.removeEventListener("deviceorientation", handleOrientation);
        };
    }, [isSupported, handleOrientation]);

    return {
        orientation,
        error,
        requestPermission,
        isSupported,
    };
}

export default useDeviceOrientation;