Lists and Carousels
Keep record relationships and full content available while a bounded horizontal comparison helps people browse related items.
Choose scanning or comparison
| Representation | Use and boundaries |
|---|---|
| List | Native ul/li for a complete sequence. Each row has a meaningful title, supporting text and one real named action; use a table when column-by-column comparison matters. |
| Carousel | A bounded horizontal collection of related items, with visible neighboring context and explicit navigation. Do not hide required information exclusively in offscreen slides. |
| Selection / destination | A centered item is a viewport position, not a chosen value, reviewed source or opened detail. An explicit action opens actual record content. |
| Variants | One/two/three-line list density and contained/peek layouts follow real content. Preserve relationships rather than clipping meaning to force identical rows. |
Anatomy and visual specifications
| Part | Actual web composition |
|---|---|
| List rows | Semantic ul/li, title and supporting text, 12px block spacing, named 44px action and decorative Separator. No row-level fake button or duplicate Tab stop. |
| Carousel region | Named/focusable div, native overflow-x:auto, inline mandatory scroll snap, 16px gap and 4px block padding. Measured inline edge space lets the first and last slides actually center. |
| Slide | Native group with slide description/name and position. Measured width 80% of the viewport, maximum 320px, readable 2px boundary, radius-lg, 16px padding and 12px internal gap. |
| Illustration | Decorative ordinal only; no fake photo, source rights or invented cultural asset. Supporting content is the actual English-only fixture. |
| Navigation / status | Ordinary Previous/Next buttons with endpoint availability. Centered index derives from actual bounding geometry after native scroll/resize, including RTL. No automatic rotation. |
Material carousel layouts and a complete vertical alternative
The reference overview lists six configurations, including the November 2025 uncontained multi-aspect-ratio addition; its guidelines introduction still lists four. They are multi-browse, uncontained, variable-aspect uncontained, hero, center-aligned hero and full-screen. Select visual emphasis and browsing intent before applying a layout name. This text-heavy web specimen uses uniform native-scroll slides and a complete filtered list, without claiming native parallax, dynamic-size masks or hero morphing.
| Reference point | Web choice and verification boundary |
|---|---|
| Layouts and item sizes | Multi-browse shows large/medium/small; hero emphasizes one with a peek; centered hero adds a leading peek. Variable-aspect uncontained supports 9:16 to 16:9 content. Native small items 40–56dp,28dp corners,16dp outer padding and 8dp gaps are references, not the local measured slide width/gap. Keep imagery recognizable and required words available at every size. |
| Text and media | Use uncontained layouts or cards for longer text. Native compact guidance favors at mostthree text-bearing visible items and brief labels; local full text wraps and stays available in the complete list. Real imagery still needs its own alternatives, source and rights; populated ordinal slides prove none of those. |
| Scrolling and full-screen | Native default free scrolling is for uncontained; other variants snap. Full-screen is portrait, compact/medium, vertically snapping and edge-to-edge rather than a free-scrolling horizontal strip. This lab uses bounded horizontal snap and geometry-derived centered position, with no autoplay or immersive full-screen claim. |
| Accessible complete access | On a vertically scrolling page, native guidance requires a vertical way to every item, often Show all or a header arrow to a dedicated list. This lab’s complete list is already rendered directly below, with the same stable IDs, filtering and detail outcomes. Previous/Next are above/below the strip, never covering its content. |
| Keyboard and state | Reference first-item/arrow navigation and generic container-role wording are not a literal web role API. The local named native scroll region and ordinary item Buttons use their documented contract; no grid/listbox role is invented. Current centered position is not a selected, read or reviewed record. |
| Reduced motion and boundaries | The reference removes parallax/size morphing under reduced motion. This uniform-size, non-autoplay strip already avoids those effects; explicit moves are immediate. End commands disable truthfully and restore focus to the surviving scroll region. Verify native RTL scrolling rather than assuming an LTR scrollLeft sign. |
Material list anatomy, expressive groups and action models
The December 2025 expressive list update adds standard/segmented styles, selection treatment and flexible slots; baseline lists remain available. Container and label are required. Consistent leading visuals, central primary/supporting text and trailing metadata/actions make scanning easier. The web example is an ordinary semantic list of independently named record actions, not a new exported expressive List component.
| Reference point | Web choice and verification boundary |
|---|---|
| One interaction model | Separate noninteractive information, one action per item, mutually exclusive selection, multi-selection and multiple independent actions. A single actionable row must not wrap secondary buttons; a selectable row needs one coherent selection interaction. This lab opens stable-ID detail; selection patterns separately demonstrate real checkbox/radio batch ownership. |
| Slots and hierarchy | Leading/trailing slots remain smaller than central content and cannot automatically guarantee accessibility. Keep visuals and labels aligned consistently. Leading avatars identify people/entities; square imagery generally describes other content. Any media/action keeps its own accessible name, alternative and real effect. |
| Native measurements and shape | Baseline 56/72/88dp item heights follow the tallest content, with top alignment at 88dp/three lines,16dp leading and 24dp trailing insets and 48dp targets. Expressive selected shapes move from 4dp inner/16dp outer corners to 16dp throughout. The local growing rows and shared control tokens do not implement that native sizing/morph engine. |
| Gaps, dividers and readable text | Prefer gaps for contained segmented groups and reserve dividers for uncontained/complex hierarchy. Native support text usually usesone to three lines and 40–60-character line lengths; preserve full meaning at enlarged text rather than silently truncating this specimen. Wide layouts should bound reading measure or use meaningful list/detail arrangements. |
| Selection cues and roles | When selection is present, show a checkbox/radio/check/other noncolor cue plus semantic state. The reference’s web listbox/option table applies to a selection model, not every ul/li containing Buttons or links. This fixture keeps ordinary document Tab order; native roving/arrow tables do not change that contract by appearance alone. |
| Adaptive detail and gestures | Native compact list navigation can become a wide list/detail layout, cards or carousel while retaining identity and actions. Swipe-to-reveal is specifically Android Views in the reference; hidden actions need explicit pointer/keyboard alternatives. This lab supplies visible controls and no native swipe, drag or container-transform physics. |
Working shared records, scroll and list access
Five loaded English-only records render in both representations. Filter titles, scroll or use navigation, and open actual content from either representation. The complete filtered list keeps every item’s text and action available without horizontal navigation.
Scroll the named comparison region or use Previous/Next. No autoplay runs. Every loaded item also appears in the complete list below.
Centered local slide 1 of 5.
Complete filtered local list
Local collection 1
English-only retained content for collection 1.
Local collection 2
English-only retained content for collection 2.
Local collection 3
English-only retained content for collection 3.
Local collection 4
English-only retained content for collection 4.
Local collection 5
English-only retained content for collection 5.
No collection opened yet.
List and carousel actions load the same stable record IDs. Filtering retains an already opened record; a centered slide is a viewport position, not a selected or reviewed source. All records render locally, without virtualization, photo rights, remote loading or invented language content.
'use client';
import {useCallback,useEffect,useMemo,useRef,useState,type CSSProperties} from 'react';
import {Button,Input,Separator} from '@mobtranslate/ui';
const records=Array.from({length:5},(_,index)=>({id:`collection-${index+1}`,title:`Local collection ${index+1}`,detail:`English-only retained content for collection ${index+1}.`}));
const action:CSSProperties={minHeight:44,height:'auto',minWidth:0,maxWidth:'100%',whiteSpace:'normal',overflowWrap:'anywhere'};
export function ListsCarouselExample(){
const [query,setQuery]=useState('');
const [current,setCurrent]=useState(0);
const [loaded,setLoaded]=useState<string|null>(null);
const [rtl,setRtl]=useState(false);
const viewport=useRef<HTMLDivElement>(null);
const slides=useRef(new Map<string,HTMLDivElement>());
const visible=useMemo(()=>records.filter(record=>record.title.toLocaleLowerCase('en').includes(query.trim().toLocaleLowerCase('en'))),[query]);
const measure=useCallback(()=>{const root=viewport.current;if(!root||!visible.length){setCurrent(0);return;}const r=root.getBoundingClientRect();const center=r.left+r.width/2;let nearest=0,distance=Infinity;visible.forEach((record,index)=>{const item=slides.current.get(record.id)?.getBoundingClientRect();if(!item)return;const next=Math.abs(item.left+item.width/2-center);if(next<distance){nearest=index;distance=next;}});setCurrent(nearest);},[visible]);
useEffect(()=>{viewport.current?.scrollTo({left:0,behavior:'auto'});setCurrent(0);},[query,rtl]);
useEffect(()=>{const root=viewport.current;if(!root)return;const observer=new ResizeObserver(()=>{const width=Math.min(root.clientWidth*.8,320);root.style.setProperty('--local-slide-width',`${width}px`);root.style.setProperty('--local-slide-edge',`${(root.clientWidth-width)/2}px`);measure();});observer.observe(root);return()=>observer.disconnect();},[measure]);
function move(index:number){const record=visible[index];if(!record)return;slides.current.get(record.id)?.scrollIntoView({block:'nearest',inline:'center',behavior:'auto'});if(index===0||index===visible.length-1)viewport.current?.focus({preventScroll:true});}
const result=records.find(record=>record.id===loaded);
return <div data-collections-lab lang="en" dir={rtl?'rtl':'ltr'} className="space-y-4">
<label htmlFor="collections-filter" className="block">Filter five loaded English-only titles</label><Input id="collections-filter" value={query} onChange={event=>setQuery(event.target.value)} />
<div className="flex flex-wrap gap-3"><Button style={action} type="button" variant="outline" onClick={()=>setQuery('')}>Clear collection filter</Button><Button style={action} type="button" variant="outline" aria-pressed={rtl} onClick={()=>setRtl(value=>!value)}>Use RTL collections</Button></div>
<p dir="auto" id="carousel-help">Scroll the named comparison region or use Previous/Next. No autoplay runs. Every loaded item also appears in the complete list below.</p>
<div ref={viewport} onScroll={measure} role="region" aria-roledescription="carousel" aria-label="Local collection comparison" aria-describedby="carousel-help" tabIndex={0} data-local-carousel className="material-carousel">
{visible.map((record,index)=><div ref={node=>{if(node)slides.current.set(record.id,node);else slides.current.delete(record.id);}} key={record.id} className="material-slide" role="group" aria-roledescription="slide" aria-label={`${index+1} of ${visible.length}: ${record.title}`} data-slide-id={record.id} data-current={current===index?'':undefined}>
<span aria-hidden="true" className="material-slide-number">{index+1}</span><h3 className="font-semibold" dir="auto">{record.title}</h3><p dir="auto">{record.detail}</p><Button style={action} type="button" variant="outline" aria-label={`Open carousel ${record.title}`} onClick={()=>setLoaded(record.id)}>Open collection</Button>
</div>)}
</div>
<div role="group" aria-label="Local carousel navigation" className="flex flex-wrap gap-3"><Button style={action} type="button" variant="outline" disabled={!visible.length||current===0} onClick={()=>move(current-1)}>Previous local slide</Button><Button style={action} type="button" variant="outline" disabled={!visible.length||current===visible.length-1} onClick={()=>move(current+1)}>Next local slide</Button></div>
<p role="status" dir="auto" data-carousel-status>{visible.length?`Centered local slide ${current+1} of ${visible.length}.`:'No loaded titles match. Clear the collection filter to recover.'}</p>
<h3 className="font-semibold">Complete filtered local list</h3><ul data-collection-list>{visible.map(record=><li key={record.id}><div className="py-3"><h4 dir="auto" className="font-semibold">{record.title}</h4><p dir="auto">{record.detail}</p><Button style={action} type="button" variant="outline" aria-label={`Open list ${record.title}`} onClick={()=>setLoaded(record.id)}>Open collection</Button></div><Separator aria-hidden="true" /></li>)}</ul>
<p role="status" dir="auto" data-collection-result>{result?`${result.title}: ${result.detail}`:'No collection opened yet.'}</p><p dir="auto">List and carousel actions load the same stable record IDs. Filtering retains an already opened record; a centered slide is a viewport position, not a selected or reviewed source. All records render locally, without virtualization, photo rights, remote loading or invented language content.</p>
</div>;
}
State and data ownership
- Filter changes the actual loaded subset and resets browsing position. No matches is an explicit empty state with Clear recovery, not a fabricated replacement record.
- Native scroll/resize determines the centered position through item and region geometry; do not assume scrollLeft signs or fixed slide widths across direction and viewport changes.
- Opening a record stores its stable ID separately from browsing position. Filtering can exclude its row while retaining its loaded result; state the policy instead of silently switching details.
- All supplied rows/slides render. There is no virtualization, remote paging, review promotion or media fetch. Large/remote datasets need separately verified focus, loading, cancellation, stale responses and completeness.
Keyboard, pointer and motion
- Tab reaches the named native scroll region and actual item actions. Previous/Next offer a non-drag navigation alternative; no custom grid/roving-tab contract is implied.
- At the final/first boundary, command navigation focuses the surviving scroll region rather than leaving focus on a newly disabled command. Verify actual centered position before announcing it.
- No autoplay runs, so reading is not interrupted. Any consuming auto-rotation needs explicit pause/stop behavior, keyboard/focus/hover handling and reduced-motion policy.
- Native inline scrolling/snap uses current direction. Explicit navigation uses the actual item’s native scrollIntoView; labels remain Previous/Next rather than ambiguous icon-only arrows.
Content and adaptive density
- Let titles and descriptions wrap; keep full content in the complete list. Do not create an image-only strip that hides attribution or required recovery.
- Treat selectable rows, destination links and commands as different semantics. One item action here loads local detail; an external destination needs a real href and appropriate navigation behavior.
- Keep the whole page reflowing while only the comparison region overflows. Narrow slides retain readable text and 44px actions; do not shrink all type to preserve desktop card counts.
- Use real image rights/source metadata when adding media. Ordinal illustrations and populated slides do not prove that a language collection is complete or culturally authorized.
API and implementation
| Owner | Actual contract |
|---|---|
| List / slides | Native ul/li and named group parts with stable IDs. No new List/Carousel package export is claimed. |
| Record action | Exported Button type=button with actual stable-ID loading handler; surrounding text supplies record context. |
| Divider | Exported Separator used as a decorative row boundary, without creating a destination or extra control. |
| Scroll / measurement | Native container ref/onScroll, per-ID slide refs, ResizeObserver with cleanup and geometry-derived position. No guessed pointer coordinates or semantic string routing. |
| Filter / recovery | Controlled Input and Clear Button over declared loaded title fields; loaded detail is a separate state owner. |
Failure cases and verification
- Avoid inaccessible autoplay, full-width page overflow, index counters that change without actual movement, offscreen-only essential content and selection states inferred from scroll position.
- Run test:material-patterns for actual navigation and geometry, native scrolling/RTL, shared list/detail outcomes, filtering/empty recovery/retention, endpoints/focus, target sizes, both themes and 320px reflow.
- Hardware swipe physics, image/remote loading, huge datasets, custom rotation and spoken assistive-technology behavior require separately scoped verification.