diff --git a/.changeset/scroll-area-content-props.md b/.changeset/scroll-area-content-props.md
new file mode 100644
index 000000000..42fc48bbb
--- /dev/null
+++ b/.changeset/scroll-area-content-props.md
@@ -0,0 +1,5 @@
+---
+'@radix-ui/react-scroll-area': minor
+---
+
+Added `contentProps` to `ScrollArea.Viewport` for forwarding props to the internal content wrapper div, enabling layout customization (e.g. overriding the default `display: table`) without `!important`.
diff --git a/apps/storybook/stories/scroll-area.stories.tsx b/apps/storybook/stories/scroll-area.stories.tsx
index d7a9aadf5..1d4a97a50 100644
--- a/apps/storybook/stories/scroll-area.stories.tsx
+++ b/apps/storybook/stories/scroll-area.stories.tsx
@@ -105,6 +105,51 @@ export const Animated = () => {
);
};
+export const ContentProps = () => {
+ return (
+
+
Default (display: table)
+
+ {Array.from({ length: 20 }).map((_, index) => (
+
+ Item {index + 1} - This is a long text that should be constrained by the container width
+
+ ))}
+
+
+
With contentProps (display: flex)
+
+
+ {Array.from({ length: 20 }).map((_, index) => (
+
+ Item {index + 1} - This is a long text that should be constrained by the container
+ width
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
export const Chromatic = () => (
Vertical
diff --git a/packages/react/scroll-area/src/scroll-area.test.tsx b/packages/react/scroll-area/src/scroll-area.test.tsx
new file mode 100644
index 000000000..34fdb7df1
--- /dev/null
+++ b/packages/react/scroll-area/src/scroll-area.test.tsx
@@ -0,0 +1,103 @@
+import * as React from 'react';
+import { cleanup, render } from '@testing-library/react';
+import { afterEach, describe, it, expect } from 'vitest';
+import * as ScrollArea from './scroll-area';
+
+const VIEWPORT_TEST_ID = 'scroll-area-viewport';
+const CONTENT_TEXT = 'Scroll content';
+
+const ScrollAreaTest = (props: {
+ contentProps?: React.HTMLAttributes
;
+}) => (
+
+
+ {CONTENT_TEXT}
+
+
+
+
+
+);
+
+describe('ScrollArea', () => {
+ afterEach(cleanup);
+
+ describe('given a default ScrollArea', () => {
+ it('should render content inside the viewport', () => {
+ const rendered = render();
+ expect(rendered.getByText(CONTENT_TEXT)).toBeInTheDocument();
+ });
+
+ it('should render the internal content wrapper with default display: table', () => {
+ const rendered = render();
+ const viewport = rendered.getByTestId(VIEWPORT_TEST_ID);
+ const contentWrapper = viewport.firstElementChild as HTMLElement;
+ expect(contentWrapper).toBeTruthy();
+ expect(contentWrapper.style.display).toBe('table');
+ expect(contentWrapper.style.minWidth).toBe('100%');
+ });
+ });
+
+ describe('given contentProps', () => {
+ it('should forward style overrides to the content wrapper', () => {
+ const rendered = render(
+
+ );
+ const viewport = rendered.getByTestId(VIEWPORT_TEST_ID);
+ const contentWrapper = viewport.firstElementChild as HTMLElement;
+ expect(contentWrapper.style.display).toBe('flex');
+ expect(contentWrapper.style.flexDirection).toBe('column');
+ // minWidth should still be present as a base style
+ expect(contentWrapper.style.minWidth).toBe('100%');
+ });
+
+ it('should forward className to the content wrapper', () => {
+ const rendered = render(
+
+ );
+ const viewport = rendered.getByTestId(VIEWPORT_TEST_ID);
+ const contentWrapper = viewport.firstElementChild as HTMLElement;
+ expect(contentWrapper.classList.contains('custom-content')).toBe(true);
+ });
+
+ it('should forward data attributes to the content wrapper', () => {
+ const rendered = render(
+
+ );
+ const viewport = rendered.getByTestId(VIEWPORT_TEST_ID);
+ const contentWrapper = viewport.firstElementChild as HTMLElement;
+ expect(contentWrapper.getAttribute('data-slot')).toBe('content');
+ });
+
+ it('should allow overriding only display while preserving minWidth', () => {
+ const rendered = render(
+
+ );
+ const viewport = rendered.getByTestId(VIEWPORT_TEST_ID);
+ const contentWrapper = viewport.firstElementChild as HTMLElement;
+ expect(contentWrapper.style.display).toBe('block');
+ expect(contentWrapper.style.minWidth).toBe('100%');
+ });
+
+ it('should allow overriding minWidth', () => {
+ const rendered = render(
+
+ );
+ const viewport = rendered.getByTestId(VIEWPORT_TEST_ID);
+ const contentWrapper = viewport.firstElementChild as HTMLElement;
+ expect(contentWrapper.style.minWidth).toBe('0');
+ expect(contentWrapper.style.display).toBe('table');
+ });
+
+ it('should still render children correctly when contentProps is provided', () => {
+ const rendered = render(
+
+ );
+ expect(rendered.getByText(CONTENT_TEXT)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/react/scroll-area/src/scroll-area.tsx b/packages/react/scroll-area/src/scroll-area.tsx
index 2db5b971d..50faa49e9 100644
--- a/packages/react/scroll-area/src/scroll-area.tsx
+++ b/packages/react/scroll-area/src/scroll-area.tsx
@@ -135,11 +135,21 @@ const VIEWPORT_NAME = 'ScrollAreaViewport';
type ScrollAreaViewportElement = React.ComponentRef;
interface ScrollAreaViewportProps extends PrimitiveDivProps {
nonce?: string;
+ /**
+ * Optional props to pass to the inner content wrapper `div`.
+ * Useful for overriding the default `display: table` layout when it conflicts
+ * with your content's sizing needs (e.g. flex or grid layouts).
+ *
+ * Note: The content wrapper uses `display: table` by default to ensure accurate
+ * scroll width/height measurement for thumb sizing. Overriding `display` may
+ * affect thumb size accuracy for horizontally-scrolling content.
+ */
+ contentProps?: React.HTMLAttributes;
}
const ScrollAreaViewport = React.forwardRef(
(props: ScopedProps, forwardedRef) => {
- const { __scopeScrollArea, children, nonce, ...viewportProps } = props;
+ const { __scopeScrollArea, children, nonce, contentProps, ...viewportProps } = props;
const context = useScrollAreaContext(VIEWPORT_NAME, __scopeScrollArea);
const ref = React.useRef(null);
const composedRefs = useComposedRefs(forwardedRef, ref, context.onViewportChange);
@@ -180,7 +190,11 @@ const ScrollAreaViewport = React.forwardRef
+
{children}