diff --git a/.changeset/free-crabs-watch.md b/.changeset/free-crabs-watch.md
new file mode 100644
index 000000000..62759a335
--- /dev/null
+++ b/.changeset/free-crabs-watch.md
@@ -0,0 +1,5 @@
+---
+'@radix-ui/react-direction': patch
+---
+
+Respect document-level dir attribute in useDirection when no DirectionProvider is present
diff --git a/packages/react/direction/src/direction.test.tsx b/packages/react/direction/src/direction.test.tsx
new file mode 100644
index 000000000..f2a749f0d
--- /dev/null
+++ b/packages/react/direction/src/direction.test.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import type { RenderResult } from '@testing-library/react';
+import { cleanup, render } from '@testing-library/react';
+import { DirectionProvider, useDirection } from './direction';
+import { afterEach, describe, it, beforeEach, expect } from 'vitest';
+
+// Helper component that renders the resolved direction as text
+function DirectionDisplay({ localDir }: { localDir?: 'ltr' | 'rtl' }) {
+ const direction = useDirection(localDir);
+ return {direction};
+}
+
+describe('useDirection', () => {
+ let rendered: RenderResult;
+
+ afterEach(() => {
+ cleanup();
+ // Reset document dir after each test
+ document.documentElement.dir = '';
+ });
+
+ describe('given no DirectionProvider and no local dir', () => {
+ describe('when document dir is not set', () => {
+ beforeEach(() => {
+ document.documentElement.dir = '';
+ rendered = render();
+ });
+
+ it('should default to ltr', () => {
+ expect(rendered.getByTestId('direction').textContent).toBe('ltr');
+ });
+ });
+
+ describe('when document dir is rtl', () => {
+ beforeEach(() => {
+ document.documentElement.dir = 'rtl';
+ rendered = render();
+ });
+
+ it('should return rtl from document', () => {
+ expect(rendered.getByTestId('direction').textContent).toBe('rtl');
+ });
+ });
+
+ describe('when document dir is ltr', () => {
+ beforeEach(() => {
+ document.documentElement.dir = 'ltr';
+ rendered = render();
+ });
+
+ it('should return ltr from document', () => {
+ expect(rendered.getByTestId('direction').textContent).toBe('ltr');
+ });
+ });
+ });
+
+ describe('given a DirectionProvider with dir="rtl"', () => {
+ describe('when document dir is ltr', () => {
+ beforeEach(() => {
+ document.documentElement.dir = 'ltr';
+ rendered = render(
+
+
+ ,
+ );
+ });
+
+ it('should return rtl from provider (overrides document)', () => {
+ expect(rendered.getByTestId('direction').textContent).toBe('rtl');
+ });
+ });
+ });
+
+ describe('given a DirectionProvider with dir="ltr"', () => {
+ describe('when document dir is rtl', () => {
+ beforeEach(() => {
+ document.documentElement.dir = 'rtl';
+ rendered = render(
+
+
+ ,
+ );
+ });
+
+ it('should return ltr from provider (overrides document)', () => {
+ expect(rendered.getByTestId('direction').textContent).toBe('ltr');
+ });
+ });
+ });
+
+ describe('given a local dir prop', () => {
+ describe('when DirectionProvider is rtl and document is rtl', () => {
+ beforeEach(() => {
+ document.documentElement.dir = 'rtl';
+ rendered = render(
+
+
+ ,
+ );
+ });
+
+ it('should return ltr from local dir (highest priority)', () => {
+ expect(rendered.getByTestId('direction').textContent).toBe('ltr');
+ });
+ });
+ });
+});
diff --git a/packages/react/direction/src/direction.tsx b/packages/react/direction/src/direction.tsx
index 9ab26b44a..307dabfd7 100644
--- a/packages/react/direction/src/direction.tsx
+++ b/packages/react/direction/src/direction.tsx
@@ -20,9 +20,16 @@ const DirectionProvider: React.FC = (props) => {
function useDirection(localDir?: Direction) {
const globalDir = React.useContext(DirectionContext);
- return localDir || globalDir || 'ltr';
+ const documentDir = getDocumentDirection();
+ return localDir || globalDir || documentDir;
}
+function getDocumentDirection(): Direction {
+ if (typeof document === 'undefined') return 'ltr';
+ return document.documentElement.dir === 'rtl' ? 'rtl' : 'ltr';
+}
+
+
const Provider = DirectionProvider;
export {