diff --git a/.storybook-s2/docs/Migrating.jsx b/.storybook-s2/docs/Migrating.jsx
index 253e46f6080..76c44aa0fd7 100644
--- a/.storybook-s2/docs/Migrating.jsx
+++ b/.storybook-s2/docs/Migrating.jsx
@@ -259,6 +259,7 @@ export function Migrating() {
If within Picker: Update Item to be a PickerItem
If within ComboBox: Update Item to be a ComboBoxItem
If within ListBox: Update Item to be a ListBoxItem
+
If within ListView: Update Item to be a ListViewItem
If within TabList: Update Item to be a Tab
If within TabPanels: Update Item to be a TabPanel and remove surrounding TabPanels
Update key to be id (and keep key if rendered inside array.map)
@@ -275,6 +276,12 @@ export function Migrating() {
Update Item to be a ListBoxItem
+
ListView
+
+
[PENDING] Comment out density (it has not been implemented yet)
+
[PENDING] Comment out dragAndDropHooks (it has not been implemented yet)
+
+
Menu
Update Item to be a MenuItem
diff --git a/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx b/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx
new file mode 100644
index 00000000000..b59ee06e31d
--- /dev/null
+++ b/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {ActionButton, ActionButtonGroup, ActionMenu, Content, Heading, IllustratedMessage, Image, ListView, ListViewItem, MenuItem, Text} from '../src';
+import {checkers} from './check';
+import Delete from '../s2wf-icons/S2_Icon_Delete_20_N.svg';
+import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg';
+import File from '../s2wf-icons/S2_Icon_File_20_N.svg';
+import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg';
+import FolderOpen from '../spectrum-illustrations/linear/FolderOpen';
+import type {Meta, StoryObj} from '@storybook/react';
+import {style} from '../style/spectrum-theme' with {type: 'macro'};
+
+const meta: Meta = {
+ component: ListView,
+ parameters: {
+ chromaticProvider: {disableAnimations: true}
+ },
+ title: 'S2 Chromatic/ListView'
+};
+
+export default meta;
+type Story = StoryObj;
+
+let listViewStyles = style({width: 320, height: 320});
+
+const items = [
+ {id: 'utilities', name: 'Utilities', type: 'folder', description: '16 items'},
+ {id: 'photoshop', name: 'Adobe Photoshop', type: 'file', description: 'Application'},
+ {id: 'illustrator', name: 'Adobe Illustrator', type: 'file', description: 'Application'},
+ {id: 'xd', name: 'Adobe XD', type: 'file', description: 'Application'}
+];
+
+export const Example: Story = {
+ render: (args) => (
+
+
+
+ Utilities
+ 16 items
+
+ Adobe Photoshop
+ Adobe Illustrator
+ Adobe XD
+
+ ),
+ args: {
+ selectionMode: 'multiple',
+ onLoadMore: undefined
+ }
+};
+
+export const HighlightSelection: Story = {
+ ...Example,
+ args: {
+ ...Example.args,
+ selectionStyle: 'highlight',
+ selectedKeys: ['photoshop', 'illustrator']
+ }
+};
+
+export const Quiet: Story = {
+ ...Example,
+ args: {
+ ...Example.args,
+ isQuiet: true
+ }
+};
+
+export const WithImages: Story = {
+ render: (args) => (
+
+ {(item) => (
+
+ {item.name}
+
+
+ )}
+
+ )
+};
+
+export const OverflowTruncate: Story = {
+ render: (args) => (
+
+
+ This is a very very very very very very very very long title.
+
+
+ Short title
+ This is a very very very very very very very very long description.
+
+
+ This is a very very very very very very very very long title.
+ This is a very very very very very very very very long description.
+
+
+ ),
+ args: {
+ ...Example.args,
+ overflowMode: 'truncate'
+ }
+};
+
+export const OverflowWrap: Story = {
+ render: (args) => (
+
+
+ This is a very very very very very very very very long title.
+
+
+ Short title
+ This is a very very very very very very very very long description.
+
+
+ This is a very very very very very very very very long title.
+ This is a very very very very very very very very long description.
+
+
+ ),
+ args: {
+ ...Example.args,
+ overflowMode: 'wrap'
+ }
+};
+
+export const DisabledItems: Story = {
+ render: (args) => (
+
+ {(item) => (
+
+ {item.type === 'folder' ? : }
+ {item.name}
+ {item.description}
+
+ )}
+
+ )
+};
+
+export const DisabledBehaviorSelection: Story = {
+ ...DisabledItems,
+ args: {
+ disabledBehavior: 'selection'
+ }
+};
+
+export const CheckboxSelection: Story = {
+ ...Example,
+ args: {
+ ...Example.args,
+ selectionStyle: 'checkbox',
+ selectedKeys: ['photoshop', 'illustrator']
+ }
+};
+
+export const Links: Story = {
+ render: (args) => (
+
+ Adobe
+ Google
+ Apple
+ New York Times
+
+ ),
+ args: {
+ selectionMode: 'none'
+ }
+};
+
+export const WithActions: Story = {
+ render: (args) => (
+
+
+
+ Utilities
+ 16 items
+
+
+
+
+
+
+
+
+
+ Adobe Photoshop
+ Application
+
+
+
+
+
+
+
+
+
+ ),
+ args: {
+ selectionMode: 'single'
+ }
+};
+
+export const Loading: Story = {
+ render: (args) => (
+
+ {[]}
+
+ ),
+ args: {
+ loadingState: 'loading'
+ }
+};
+
+export const LoadingMore: Story = {
+ render: (args) => (
+ {}}>
+ {(item) => (
+
+ {item.name}
+
+ )}
+
+ ),
+ args: {
+ loadingState: 'loadingMore'
+ }
+};
+
+export const EmptyState: Story = {
+ render: (args) => (
+ (
+
+
+ No results
+ No results found.
+
+ )}>
+ {[]}
+
+ )
+};
diff --git a/packages/@react-spectrum/s2/chromatic/check.tsx b/packages/@react-spectrum/s2/chromatic/check.tsx
new file mode 100644
index 00000000000..c461a431e3d
--- /dev/null
+++ b/packages/@react-spectrum/s2/chromatic/check.tsx
@@ -0,0 +1,2 @@
+
+export let checkers = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAuMTAwMDk4IDBIMy4xMDAxVjNIMC4xMDAwOThWMFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTAuMTAwMDk4IDE4SDMuMTAwMVYyMUgwLjEwMDA5OFYxOFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTYuMTAwMSAwSDkuMTAwMVYzSDYuMTAwMVYwWiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNNi4xMDAxIDE4SDkuMTAwMVYyMUg2LjEwMDFWMThaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0xMi4xMDAxIDE4SDkuMTAwMVYxNUgxMi4xMDAxVjE4WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTIuMTAwMSAyNEg5LjEwMDFWMjFIMTIuMTAwMVYyNFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTE1LjEwMDEgMEgxMi4xMDAxVjNIMTUuMTAwMVYwWiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTUuMTAwMSAxOEgxMi4xMDAxVjIxSDE1LjEwMDFWMThaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0xOC4xMDAxIDE4SDE1LjEwMDFWMTVIMTguMTAwMVYxOFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTE4LjEwMDEgMjRIMTUuMTAwMVYyMUgxOC4xMDAxVjI0WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMjEuMTAwMSAwSDE4LjEwMDFWM0gyMS4xMDAxVjBaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDE4SDE4LjEwMDFWMjFIMjEuMTAwMVYxOFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTI0LjEwMDEgMThIMjEuMTAwMVYxNUgyNC4xMDAxVjE4WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMjEuMTAwMSAyMUgyNC4xMDAxVjI0SDIxLjEwMDFWMjFaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik02LjEwMDEgMThIMy4xMDAxVjE1SDYuMTAwMVYxOFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTYuMTAwMSAyNEgzLjEwMDFWMjFINi4xMDAxVjI0WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMC4xMDAwOTggNkgzLjEwMDFWM0gwLjEwMDA5OFY2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuMTAwMSA2SDkuMTAwMVYzSDYuMTAwMVY2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEyLjEwMDEgMTJIOS4xMDAxVjE1SDEyLjEwMDFWMTJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTUuMTAwMSA2SDEyLjEwMDFWM0gxNS4xMDAxVjZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTguMTAwMSAxMkgxNS4xMDAxVjE1SDE4LjEwMDFWMTJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjEuMTAwMSA2SDE4LjEwMDFWM0gyMS4xMDAxVjZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjQuMTAwMSAxMkgyMS4xMDAxVjE1SDI0LjEwMDFWMTJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNi4xMDAxIDEySDMuMTAwMVYxNUg2LjEwMDFWMTJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMC4xMDAwOTggOUgzLjEwMDFWNkgwLjEwMDA5OFY5WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNNi4xMDAxIDlIOS4xMDAxVjZINi4xMDAxVjlaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0xMi4xMDAxIDlIOS4xMDAxVjEySDEyLjEwMDFWOVoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTE1LjEwMDEgOUgxMi4xMDAxVjZIMTUuMTAwMVY5WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTguMTAwMSA5SDE1LjEwMDFWMTJIMTguMTAwMVY5WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMjEuMTAwMSA5SDE4LjEwMDFWNkgyMS4xMDAxVjlaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0yNC4xMDAxIDlIMjEuMTAwMVYxMkgyNC4xMDAxVjlaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik02LjEwMDEgOUgzLjEwMDFWMTJINi4xMDAxVjlaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0wLjEwMDA5OCAxMkgzLjEwMDFWOUgwLjEwMDA5OFYxMloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik02LjEwMDEgMTJIOS4xMDAxVjlINi4xMDAxVjEyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEyLjEwMDEgNkg5LjEwMDFWOUgxMi4xMDAxVjZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTUuMTAwMSAxMkgxMi4xMDAxVjlIMTUuMTAwMVYxMloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xOC4xMDAxIDZIMTUuMTAwMVY5SDE4LjEwMDFWNloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDEySDE4LjEwMDFWOUgyMS4xMDAxVjEyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTI0LjEwMDEgNkgyMS4xMDAxVjlIMjQuMTAwMVY2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuMTAwMSA2SDMuMTAwMVY5SDYuMTAwMVY2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTAuMTAwMDk4IDE1SDMuMTAwMVYxMkgwLjEwMDA5OFYxNVoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTYuMTAwMSAxNUg5LjEwMDFWMTJINi4xMDAxVjE1WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTIuMTAwMSAzSDkuMTAwMVY2SDEyLjEwMDFWM1oiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTE1LjEwMDEgMTVIMTIuMTAwMVYxMkgxNS4xMDAxVjE1WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTguMTAwMSAzSDE1LjEwMDFWNkgxOC4xMDAxVjNaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDE1SDE4LjEwMDFWMTJIMjEuMTAwMVYxNVoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTI0LjEwMDEgM0gyMS4xMDAxVjZIMjQuMTAwMVYzWiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNNi4xMDAxIDNIMy4xMDAxVjZINi4xMDAxVjNaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0wLjEwMDA5OCAxOEgzLjEwMDFWMTVIMC4xMDAwOThWMThaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMC4xMDAwOTggMjRIMy4xMDAxVjIxSDAuMTAwMDk4VjI0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuMTAwMSAxOEg5LjEwMDFWMTVINi4xMDAxVjE4WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuMTAwMSAyNEg5LjEwMDFWMjFINi4xMDAxVjI0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEyLjEwMDEgMEg5LjEwMDFWM0gxMi4xMDAxVjBaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTIuMTAwMSAxOEg5LjEwMDFWMjFIMTIuMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNS4xMDAxIDE4SDEyLjEwMDFWMTVIMTUuMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNS4xMDAxIDI0SDEyLjEwMDFWMjFIMTUuMTAwMVYyNFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xOC4xMDAxIDBIMTUuMTAwMVYzSDE4LjEwMDFWMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xOC4xMDAxIDE4SDE1LjEwMDFWMjFIMTguMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDE4SDE4LjEwMDFWMTVIMjEuMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDI0SDE4LjEwMDFWMjFIMjEuMTAwMVYyNFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDNIMjQuMTAwMVYwSDIxLjEwMDFWM1oiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yNC4xMDAxIDE4SDIxLjEwMDFWMjFIMjQuMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik02LjEwMDEgMEgzLjEwMDFWM0g2LjEwMDFWMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik02LjEwMDEgMThIMy4xMDAxVjIxSDYuMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=';
diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx
new file mode 100644
index 00000000000..05cb31186d3
--- /dev/null
+++ b/packages/@react-spectrum/s2/src/ListView.tsx
@@ -0,0 +1,791 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {ActionButtonGroupContext} from './ActionButtonGroup';
+import {ActionMenuContext} from './ActionMenu';
+import {baseColor, colorMix, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'};
+import {centerBaseline} from './CenterBaseline';
+import {Checkbox} from './Checkbox';
+import {
+ CheckboxContext,
+ Collection,
+ CollectionRendererContext,
+ ContextValue,
+ DEFAULT_SLOT,
+ DefaultCollectionRenderer,
+ GridList,
+ GridListItem,
+ GridListItemProps,
+ GridListItemRenderProps,
+ GridListLoadMoreItem,
+ GridListProps,
+ GridListRenderProps,
+ Key,
+ ListLayout,
+ ListState,
+ Provider,
+ SlotProps,
+ useSlottedContext,
+ Virtualizer
+} from 'react-aria-components';
+import Chevron from '../ui-icons/Chevron';
+import {controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
+import {createContext, forwardRef, ReactElement, ReactNode, useContext, useRef} from 'react';
+import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LoadingState} from '@react-types/shared';
+import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'};
+import {IconContext} from './Icon';
+import {ImageContext} from './Image';
+// @ts-ignore
+import intlMessages from '../intl/*.json';
+import LinkOutIcon from '../ui-icons/LinkOut';
+import {ProgressCircle} from './ProgressCircle';
+import {Text, TextContext} from './Content';
+import {useActionBarContainer} from './ActionBar';
+import {useDOMRef} from '@react-spectrum/utils';
+import {useLocale, useLocalizedStringFormatter} from 'react-aria';
+import {useScale} from './utils';
+import {useSpectrumContextProps} from './useSpectrumContextProps';
+
+export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | 'dragAndDropHooks' | 'layout' | 'render' | 'keyboardNavigationBehavior' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps {
+ /** Spectrum-defined styles, returned by the `style()` macro. */
+ styles?: StylesPropWithHeight,
+ /** The current loading state of the ListView. */
+ loadingState?: LoadingState,
+ /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */
+ onLoadMore?: () => void,
+ /** The children of the ListView. */
+ children: ReactNode | ((item: T) => ReactNode),
+ /** Provides the ActionBar to display when items are selected in the ListView. */
+ renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement,
+ /** Hides the default link out icons on items that open links in a new tab. */
+ hideLinkOutIcon?: boolean
+}
+
+interface ListViewStylesProps {
+ /** Whether the ListView should be displayed with a quiet style. */
+ isQuiet?: boolean,
+ /**
+ * How selection should be displayed.
+ * @default 'checkbox'
+ */
+ selectionStyle?: 'highlight' | 'checkbox',
+ /**
+ * Sets the overflow behavior for item contents.
+ * @default 'truncate'
+ */
+ overflowMode?: 'wrap' | 'truncate'
+}
+
+export interface ListViewItemProps extends Omit {
+ /**
+ * The contents of the item.
+ */
+ children: ReactNode,
+ /** Whether the item has child items (renders a chevron indicator). */
+ hasChildItems?: boolean
+}
+
+export const ListViewContext = createContext>, DOMRefValue>>(null);
+
+let InternalListViewContext = createContext<{isQuiet?: boolean, selectionStyle?: 'highlight' | 'checkbox', overflowMode?: 'wrap' | 'truncate', scale?: 'medium' | 'large', hideLinkOutIcon?: boolean}>({});
+
+const listViewWrapper = style({
+ minHeight: 0,
+ minWidth: 0,
+ display: 'flex',
+ isolation: 'isolate',
+ disableTapHighlight: true,
+ position: 'relative',
+ // Clip ActionBar animation.
+ overflow: 'clip'
+}, getAllowedOverrides({height: true}));
+
+// When any row has a trailing icon, reserve space so actions align.
+const hasTrailingIconRows = ':has([data-has-trailing-icon]) [role="row"]';
+
+const listView = style({
+ ...focusRing(),
+ outlineOffset: {
+ default: -2,
+ isQuiet: -1
+ },
+ userSelect: 'none',
+ minHeight: 0,
+ minWidth: 0,
+ width: 'full',
+ height: 'full',
+ boxSizing: 'border-box',
+ overflow: 'auto',
+ fontSize: controlFont(),
+ backgroundColor: {
+ default: 'gray-25',
+ isQuiet: 'transparent',
+ forcedColors: 'Background'
+ },
+ borderRadius: {
+ default: 'default',
+ isQuiet: 'none'
+ },
+ borderColor: 'gray-300',
+ borderWidth: {
+ default: 1,
+ isQuiet: 0
+ },
+ borderStyle: 'solid',
+ '--trailing-icon-width': {
+ type: 'width',
+ value: {
+ default: 'auto',
+ [hasTrailingIconRows]: fontRelative(20)
+ }
+ }
+});
+
+/**
+ * A ListView displays a list of interactive items, and allows a user to navigate, select, or perform an action.
+ */
+export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function ListView(
+ props: ListViewProps,
+ ref: DOMRef
+) {
+ [props, ref] = useSpectrumContextProps(props, ref, ListViewContext);
+ let {children, isQuiet, selectionStyle = 'checkbox', overflowMode = 'truncate', loadingState, onLoadMore, renderEmptyState: userRenderEmptyState, hideLinkOutIcon = false, ...otherProps} = props;
+ let scale = useScale();
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
+ let rowHeight = scale === 'large' ? 50 : 40;
+
+ let domRef = useDOMRef(ref);
+ let scrollRef = useRef(null);
+
+ let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
+ let renderEmptyState: ListViewProps['renderEmptyState'] | undefined;
+ if (userRenderEmptyState != null && !isLoading) {
+ renderEmptyState = (renderProps) => (
+
+ )}
+
+ );
+ }}
+
+ );
+}
+
diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts
index e2e27f4655f..069647cbb85 100644
--- a/packages/@react-spectrum/s2/src/index.ts
+++ b/packages/@react-spectrum/s2/src/index.ts
@@ -57,6 +57,7 @@ export {Image, ImageContext} from './Image';
export {ImageCoordinator} from './ImageCoordinator';
export {InlineAlert, InlineAlertContext} from './InlineAlert';
export {Link, LinkContext} from './Link';
+export {ListView, ListViewContext, ListViewItem} from './ListView';
export {MenuItem, MenuTrigger, Menu, MenuSection, SubmenuTrigger, UnavailableMenuItemTrigger, MenuContext} from './Menu';
export {Meter, MeterContext} from './Meter';
export {NotificationBadge, NotificationBadgeContext} from './NotificationBadge';
@@ -137,6 +138,7 @@ export type {InlineAlertProps} from './InlineAlert';
export type {ImageProps} from './Image';
export type {ImageCoordinatorProps} from './ImageCoordinator';
export type {LinkProps} from './Link';
+export type {ListViewProps, ListViewItemProps} from './ListView';
export type {MenuTriggerProps, MenuProps, MenuItemProps, MenuSectionProps, SubmenuTriggerProps, UnavailableMenuItemTriggerProps} from './Menu';
export type {MeterProps} from './Meter';
export type {NotificationBadgeProps} from './NotificationBadge';
diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx
new file mode 100644
index 00000000000..d91c0a26820
--- /dev/null
+++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx
@@ -0,0 +1,583 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {action} from 'storybook/actions';
+import {ActionBar, ActionButton, ActionButtonGroup, ActionMenu, Breadcrumb, Breadcrumbs, Content, Heading, IllustratedMessage, Image, ListView, ListViewItem, MenuItem, Text} from '../';
+import {categorizeArgTypes} from './utils';
+import {chain} from '@react-aria/utils';
+import Copy from '../s2wf-icons/S2_Icon_Copy_20_N.svg';
+import Delete from '../s2wf-icons/S2_Icon_Delete_20_N.svg';
+import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg';
+import File from '../s2wf-icons/S2_Icon_File_20_N.svg';
+import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg';
+import FolderOpen from '../spectrum-illustrations/linear/FolderOpen';
+import {Key} from 'react-aria';
+import type {Meta, StoryObj} from '@storybook/react';
+import {ReactNode, useState} from 'react';
+import {style} from '../style' with {type: 'macro'};
+import {useAsyncList} from 'react-stately';
+
+const meta: Meta = {
+ component: ListView,
+ parameters: {
+ layout: 'centered'
+ },
+ tags: ['autodocs'],
+ argTypes: {
+ ...categorizeArgTypes('Events', ['onSelectionChange'])
+ },
+ title: 'ListView',
+ args: {
+ styles: style({height: 320})
+ },
+ decorators: [
+ (Story) => (
+
+```
+
+### Asynchronous loading
+
+Use the `loadingState` and `onLoadMore` props to enable async loading and infinite scrolling.
+
+```tsx render type="s2"
+"use client";
+import {ListView, ListViewItem} from '@react-spectrum/s2';
+import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
+import {useAsyncList} from 'react-stately';
+
+interface Character {
+ name: string
+}
+
+function AsyncListView() {
+ let list = useAsyncList({
+ async load({signal, cursor}) {
+ if (cursor) {
+ cursor = cursor.replace(/^http:\/\//i, 'https://');
+ }
+
+ let res = await fetch(cursor || 'https://swapi.py4e.com/api/people/?search=', {signal});
+ let json = await res.json();
+ return {
+ items: json.results,
+ cursor: json.next
+ };
+ }
+ });
+
+ return (
+
+ {item => (
+ {item.name}
+ )}
+
+ );
+}
+```
+
+### Links
+
+Use the `href` prop on a `ListViewItem` to create a link. See the [getting started guide](getting-started) to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection) for more details.
+
+```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'isQuiet', 'hideLinkOutIcon']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'none'}} type="s2"
+import {ListView, ListViewItem} from '@react-spectrum/s2';
+import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
+
+
+
+ adobe.com
+
+
+ spectrum.adobe.com
+
+
+ react-spectrum.adobe.com
+
+
+```
+
+### Empty state
+
+Use `renderEmptyState` to render placeholder content when there are no items.
+
+```tsx render type="s2"
+"use client";
+import {ListView, IllustratedMessage, Heading, Content, Link} from '@react-spectrum/s2';
+import FolderOpen from '@react-spectrum/s2/illustrations/linear/FolderOpen';
+import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
+
+ (
+
+
+ No results
+ Press here for more info.
+
+ )}>
+ {/*- end highlight -*/}
+ {[]}
+
+```
+
+## Selection and actions
+
+Use the `selectionMode` prop to enable single or multiple selection. The selected items can be controlled via the `selectedKeys` prop, matching the `id` prop of the items. The `onAction` event handles item actions. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=ListView) for more details.
+
+```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle', 'isQuiet', 'disabledBehavior', 'disallowEmptySelection']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2"
+"use client";
+import {ListView, ListViewItem, ActionBar, ActionButton, type Selection} from '@react-spectrum/s2';
+import Edit from '@react-spectrum/s2/icons/Edit';
+import Copy from '@react-spectrum/s2/icons/Copy';
+import Delete from '@react-spectrum/s2/icons/Delete';
+import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
+import {useState} from 'react';
+
+function Example(props) {
+ let [selected, setSelected] = useState(new Set());
+
+ return (
+