aria-posinset
Defines an element's number or position in the current set of listitems or treeitems when not all items are present in the DOM.
Overview
The aria-posinset attribute defines an element's position (1-based index) within a set. It's typically used with aria-setsize to tell assistive technologies the total number of items.
This is essential for paginated lists, virtual scrolling, and any scenario where not all items are rendered in the DOM but users need to understand their position in the complete set.
Screen readers typically announce this as "item X of Y" when users navigate to the element.
Live Demo: Paginated List
- Search Result 11 of 50
aria-posinset="1" aria-setsize="50"
- Search Result 22 of 50
aria-posinset="2" aria-setsize="50"
- Search Result 33 of 50
aria-posinset="3" aria-setsize="50"
- Search Result 44 of 50
aria-posinset="4" aria-setsize="50"
- Search Result 55 of 50
aria-posinset="5" aria-setsize="50"
Each item has aria-posinset indicating its position and aria-setsize indicating the total. Screen readers announce "item 1 of 50".
Code Examples
Basic Usage
<!-- aria-posinset defines position within a set -->
<!-- Paginated list showing items 6-10 of 50 -->
<ul role="listbox" aria-label="Search results">
<li role="option" aria-posinset="6" aria-setsize="50">
Result 6
</li>
<li role="option" aria-posinset="7" aria-setsize="50">
Result 7
</li>
<li role="option" aria-posinset="8" aria-setsize="50">
Result 8
</li>
<li role="option" aria-posinset="9" aria-setsize="50">
Result 9
</li>
<li role="option" aria-posinset="10" aria-setsize="50">
Result 10
</li>
</ul>
<!-- Screen reader: "Result 6, 6 of 50" -->Virtual Scrolling
<!-- Virtual scrolling list with aria-posinset -->
<div role="listbox" aria-label="All users">
<!-- Only visible items rendered, but position maintained -->
<div role="option"
aria-posinset="101"
aria-setsize="5000">
User 101
</div>
<div role="option"
aria-posinset="102"
aria-setsize="5000">
User 102
</div>
<div role="option"
aria-posinset="103"
aria-setsize="5000">
User 103
</div>
<!-- ... more visible items ... -->
</div>
<!-- Enables screen reader users to understand position
in large datasets even when items aren't all rendered -->Tree Items
<!-- Tree items with aria-posinset -->
<ul role="tree" aria-label="File system">
<li role="treeitem"
aria-posinset="1"
aria-setsize="3"
aria-expanded="true">
Documents
<ul role="group">
<li role="treeitem"
aria-posinset="1"
aria-setsize="2">
Work
</li>
<li role="treeitem"
aria-posinset="2"
aria-setsize="2">
Personal
</li>
</ul>
</li>
<li role="treeitem"
aria-posinset="2"
aria-setsize="3">
Downloads
</li>
<li role="treeitem"
aria-posinset="3"
aria-setsize="3">
Pictures
</li>
</ul>
<!-- Each level has its own position counting -->React Components
// React components with aria-posinset
import { useState, useMemo } from 'react';
// Paginated list with position info
interface PaginatedListProps<T> {
items: T[];
totalItems: number;
currentPage: number;
pageSize: number;
renderItem: (item: T, position: number, total: number) => React.ReactNode;
label: string;
}
function PaginatedList<T>({
items,
totalItems,
currentPage,
pageSize,
renderItem,
label
}: PaginatedListProps<T>) {
const startIndex = (currentPage - 1) * pageSize;
return (
<ul role="listbox" aria-label={label}>
{items.map((item, index) => {
const position = startIndex + index + 1;
return (
<li
key={index}
role="option"
aria-posinset={position}
aria-setsize={totalItems}
>
{renderItem(item, position, totalItems)}
</li>
);
})}
</ul>
);
}
// Virtual list with position tracking
function VirtualList({
items,
totalItems,
visibleStartIndex,
renderItem
}) {
return (
<div role="listbox" aria-label="Virtual list">
{items.map((item, index) => {
const position = visibleStartIndex + index + 1;
return (
<div
key={position}
role="option"
aria-posinset={position}
aria-setsize={totalItems}
>
{renderItem(item, position)}
</div>
);
})}
</div>
);
}
// Tree item component
function TreeItem({
item,
position,
siblingCount,
children
}) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<li
role="treeitem"
aria-posinset={position}
aria-setsize={siblingCount}
aria-expanded={children ? isExpanded : undefined}
>
<button onClick={() => setIsExpanded(!isExpanded)}>
{item.name}
</button>
{isExpanded && children && (
<ul role="group">
{children}
</ul>
)}
</li>
);
}
// Usage
function SearchResults() {
const [page, setPage] = useState(1);
const pageSize = 10;
const totalResults = 250;
const results = fetchResults(page, pageSize);
return (
<div>
<PaginatedList
items={results}
totalItems={totalResults}
currentPage={page}
pageSize={pageSize}
label="Search results"
renderItem={(item, pos, total) => (
<span>{item.title} (item {pos} of {total})</span>
)}
/>
<nav aria-label="Pagination">
<button
onClick={() => setPage(p => p - 1)}
disabled={page === 1}
>
Previous
</button>
<button
onClick={() => setPage(p => p + 1)}
disabled={page * pageSize >= totalResults}
>
Next
</button>
</nav>
</div>
);
}
