Essential utility functions and React hooks for building accessible React Aria UI components
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Viewport tracking, scroll utilities, element positioning, and resize observation for responsive React components.
Utilities for finding scrollable ancestors and determining scroll behavior.
/**
* Finds the nearest scrollable ancestor element
* @param node - Starting element
* @param checkForOverflow - Whether to check overflow styles (default: true)
* @returns Scrollable parent element or document scrolling element
*/
function getScrollParent(node: Element, checkForOverflow?: boolean): Element;
/**
* Gets array of all scrollable ancestors
* @param node - Starting element
* @returns Array of scrollable parent elements
*/
function getScrollParents(node: Element): Element[];
/**
* Determines if element is scrollable
* @param element - Element to check
* @param checkForOverflow - Whether to check overflow styles
* @returns true if element can scroll
*/
function isScrollable(element: Element, checkForOverflow?: boolean): boolean;Usage Examples:
import { getScrollParent, getScrollParents, isScrollable } from "@react-aria/utils";
function ScrollAwareComponent() {
const elementRef = useRef<HTMLDivElement>(null);
const [scrollParent, setScrollParent] = useState<Element | null>(null);
useEffect(() => {
if (elementRef.current) {
// Find immediate scroll parent
const parent = getScrollParent(elementRef.current);
setScrollParent(parent);
// Get all scroll parents for complex layouts
const allParents = getScrollParents(elementRef.current);
console.log('All scroll parents:', allParents);
// Check if element itself is scrollable
const canScroll = isScrollable(elementRef.current);
console.log('Element can scroll:', canScroll);
}
}, []);
return (
<div ref={elementRef}>
Content that needs scroll awareness
{scrollParent && (
<p>Scroll parent: {scrollParent.tagName}</p>
)}
</div>
);
}
// Sticky positioning with scroll parent awareness
function StickyHeader() {
const headerRef = useRef<HTMLElement>(null);
const [isSticky, setIsSticky] = useState(false);
useEffect(() => {
if (!headerRef.current) return;
const scrollParent = getScrollParent(headerRef.current);
const handleScroll = () => {
const scrollTop = scrollParent.scrollTop || 0;
setIsSticky(scrollTop > 100);
};
scrollParent.addEventListener('scroll', handleScroll, { passive: true });
return () => scrollParent.removeEventListener('scroll', handleScroll);
}, []);
return (
<header
ref={headerRef}
className={isSticky ? 'sticky' : ''}
>
Header Content
</header>
);
}Utilities for scrolling elements into view with smart positioning.
/**
* Scrolls container so element is visible (like {block: 'nearest'})
* @param scrollView - Container element to scroll
* @param element - Element to bring into view
*/
function scrollIntoView(scrollView: HTMLElement, element: HTMLElement): void;
/**
* Scrolls element into viewport with overlay awareness
* @param targetElement - Element to scroll into view
* @param opts - Options for scrolling behavior
*/
function scrollIntoViewport(
targetElement: Element,
opts?: { containingElement?: Element }
): void;Usage Examples:
import { scrollIntoView, scrollIntoViewport } from "@react-aria/utils";
function ScrollableList({ items, selectedIndex }) {
const listRef = useRef<HTMLUListElement>(null);
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
// Scroll selected item into view when selection changes
useEffect(() => {
if (selectedIndex >= 0 && itemRefs.current[selectedIndex] && listRef.current) {
scrollIntoView(listRef.current, itemRefs.current[selectedIndex]!);
}
}, [selectedIndex]);
return (
<ul ref={listRef} className="scrollable-list">
{items.map((item, index) => (
<li
key={item.id}
ref={el => itemRefs.current[index] = el}
className={index === selectedIndex ? 'selected' : ''}
>
{item.name}
</li>
))}
</ul>
);
}
// Modal with smart scrolling
function Modal({ isOpen, children }) {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen && modalRef.current) {
// Scroll modal into viewport, accounting for overlays
scrollIntoViewport(modalRef.current);
}
}, [isOpen]);
return isOpen ? (
<div className="modal-backdrop">
<div ref={modalRef} className="modal-content">
{children}
</div>
</div>
) : null;
}
// Keyboard navigation with scroll
function useKeyboardNavigation(items: any[], onSelect: (index: number) => void) {
const [selectedIndex, setSelectedIndex] = useState(0);
const containerRef = useRef<HTMLElement>(null);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(prev => {
const newIndex = Math.min(prev + 1, items.length - 1);
// Scroll item into view
const container = containerRef.current;
const item = container?.children[newIndex] as HTMLElement;
if (container && item) {
scrollIntoView(container, item);
}
return newIndex;
});
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(prev => {
const newIndex = Math.max(prev - 1, 0);
const container = containerRef.current;
const item = container?.children[newIndex] as HTMLElement;
if (container && item) {
scrollIntoView(container, item);
}
return newIndex;
});
break;
case 'Enter':
e.preventDefault();
onSelect(selectedIndex);
break;
}
}, [items.length, selectedIndex, onSelect]);
return { selectedIndex, containerRef, handleKeyDown };
}Hook for tracking viewport dimensions with device-aware updates.
/**
* Tracks viewport dimensions with device-aware updates
* @returns Object with current viewport width and height
*/
function useViewportSize(): ViewportSize;
interface ViewportSize {
width: number;
height: number;
}Usage Examples:
import { useViewportSize } from "@react-aria/utils";
function ResponsiveComponent() {
const { width, height } = useViewportSize();
const isMobile = width < 768;
const isTablet = width >= 768 && width < 1024;
const isDesktop = width >= 1024;
return (
<div>
<p>Viewport: {width} x {height}</p>
<div className={`layout ${isMobile ? 'mobile' : isTablet ? 'tablet' : 'desktop'}`}>
{isMobile ? (
<MobileLayout />
) : isTablet ? (
<TabletLayout />
) : (
<DesktopLayout />
)}
</div>
</div>
);
}
// Responsive grid based on viewport
function ResponsiveGrid({ children }) {
const { width } = useViewportSize();
const columns = useMemo(() => {
if (width < 480) return 1;
if (width < 768) return 2;
if (width < 1024) return 3;
return 4;
}, [width]);
return (
<div
style={{
display: 'grid',
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: '1rem'
}}
>
{children}
</div>
);
}
// Virtual keyboard handling on mobile
function MobileForm() {
const { height } = useViewportSize();
const [initialHeight] = useState(() => window.innerHeight);
// Detect virtual keyboard
const isVirtualKeyboardOpen = height < initialHeight * 0.8;
return (
<form className={isVirtualKeyboardOpen ? 'keyboard-open' : ''}>
<input placeholder="This adjusts for virtual keyboard" />
<button type="submit">Submit</button>
</form>
);
}Hook for observing element resize with ResizeObserver API and fallbacks.
/**
* Observes element resize with ResizeObserver API
* @param options - Configuration for resize observation
*/
function useResizeObserver<T extends Element>(options: {
ref: RefObject<T>;
box?: ResizeObserverBoxOptions;
onResize: () => void;
}): void;
type ResizeObserverBoxOptions = "border-box" | "content-box" | "device-pixel-content-box";Usage Examples:
import { useResizeObserver } from "@react-aria/utils";
function ResizableComponent() {
const elementRef = useRef<HTMLDivElement>(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useResizeObserver({
ref: elementRef,
onResize: () => {
if (elementRef.current) {
const { offsetWidth, offsetHeight } = elementRef.current;
setSize({ width: offsetWidth, height: offsetHeight });
}
}
});
return (
<div ref={elementRef} style={{ resize: 'both', overflow: 'auto', border: '1px solid #ccc' }}>
<p>Resize me!</p>
<p>Current size: {size.width} x {size.height}</p>
</div>
);
}
// Responsive text sizing based on container
function ResponsiveText({ children }) {
const containerRef = useRef<HTMLDivElement>(null);
const [fontSize, setFontSize] = useState(16);
useResizeObserver({
ref: containerRef,
onResize: () => {
if (containerRef.current) {
const width = containerRef.current.offsetWidth;
// Scale font size based on container width
const newFontSize = Math.max(12, Math.min(24, width / 20));
setFontSize(newFontSize);
}
}
});
return (
<div ref={containerRef} style={{ fontSize: `${fontSize}px` }}>
{children}
</div>
);
}
// Chart that redraws on resize
function Chart({ data }) {
const chartRef = useRef<HTMLCanvasElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useResizeObserver({
ref: chartRef,
box: 'content-box',
onResize: () => {
if (chartRef.current) {
const { clientWidth, clientHeight } = chartRef.current;
setDimensions({ width: clientWidth, height: clientHeight });
}
}
});
// Redraw chart when dimensions change
useEffect(() => {
if (chartRef.current && dimensions.width > 0) {
drawChart(chartRef.current, data, dimensions);
}
}, [data, dimensions]);
return <canvas ref={chartRef} />;
}Complex scrolling and layout scenarios:
import {
useViewportSize,
useResizeObserver,
scrollIntoView,
getScrollParents
} from "@react-aria/utils";
function InfiniteScrollList({ items, onLoadMore }) {
const containerRef = useRef<HTMLDivElement>(null);
const sentinelRef = useRef<HTMLDivElement>(null);
const { height: viewportHeight } = useViewportSize();
// Resize handling for container
useResizeObserver({
ref: containerRef,
onResize: () => {
// Recalculate visible items when container resizes
updateVisibleItems();
}
});
// Intersection observer for infinite scroll
useEffect(() => {
if (!sentinelRef.current) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
onLoadMore();
}
},
{ threshold: 0.1 }
);
observer.observe(sentinelRef.current);
return () => observer.disconnect();
}, [onLoadMore]);
// Smart scrolling for keyboard navigation
const scrollToItem = useCallback((index: number) => {
const container = containerRef.current;
const item = container?.children[index] as HTMLElement;
if (container && item) {
scrollIntoView(container, item);
}
}, []);
return (
<div
ref={containerRef}
style={{ height: Math.min(viewportHeight * 0.8, 600), overflow: 'auto' }}
>
{items.map((item, index) => (
<div key={item.id}>
{item.content}
</div>
))}
<div ref={sentinelRef} style={{ height: 1 }} />
</div>
);
}Install with Tessl CLI
npx tessl i tessl/npm-react-aria--utils