Reel
A reel is a horizontally scrolling full-width container. Frames are placed side by side — if they all fit in the viewport they display without scrolling, otherwise the reel becomes scrollable. Apply the .reel class to the container and .snap-start to each child frame. Use the useReel hook from @volvo-cars/react-headless for navigation arrows and active-frame tracking.
import { useReel } from '@volvo-cars/react-headless';import { useRef } from 'react';
export function BasicReel() { const ref = useRef<HTMLDivElement>(null); const { reelProps } = useReel({ ref });
return ( <section ref={ref} className="reel gap-x-gridded-element" aria-label="Example frames" {...reelProps} > {Array.from({ length: 8 }, (_, i) => i + 1).map((frame) => ( <div key={frame} className="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style={{ minWidth: '12rem', minHeight: '8rem' }} > Frame {frame} </div> ))} </section> );}Navigation arrows
Section titled “Navigation arrows”Spread previousButtonProps and nextButtonProps onto icon buttons to add back/forward navigation. The hook manages disabled state and hides the buttons from assistive technology since the scroll container is already keyboard-navigable.
import { useReel } from '@volvo-cars/react-headless';import { IconButton } from '@volvo-cars/react-icons';import { useRef } from 'react';
export function ReelWithArrows() { const ref = useRef<HTMLDivElement>(null); const { previousButtonProps, nextButtonProps, reelProps } = useReel({ ref });
return ( <div> <section ref={ref} className="reel gap-x-gridded-element" aria-label="Scrollable frames" {...reelProps} > {Array.from({ length: 10 }, (_, i) => i + 1).map((frame) => ( <div key={frame} className="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style={{ minWidth: '12rem', minHeight: '8rem' }} > Frame {frame} </div> ))} </section> <div className="flex items-center gap-8 mt-24 justify-end"> <IconButton icon="chevron-back" variant="filled" color="subtle" {...previousButtonProps} /> <IconButton icon="chevron-forward" variant="filled" color="subtle" {...nextButtonProps} /> </div> </div> );}Card content
Section titled “Card content”Reels work well for browsing product cards. Set a width on each card and use flex-shrink-0 to prevent them from collapsing. The last card should always be partially visible to indicate more content.
import { useReel } from '@volvo-cars/react-headless';import { IconButton } from '@volvo-cars/react-icons';import { useRef } from 'react';
const cards = [ { title: 'EX30', subtitle: 'Electric SUV', image: 'ex30' }, { title: 'EX90', subtitle: 'Electric SUV', image: 'ex90' }, { title: 'XC90', subtitle: 'Plug-in hybrid SUV', image: 'xc90' }, { title: 'XC60', subtitle: 'Plug-in hybrid SUV', image: 'xc60' }, { title: 'XC40', subtitle: 'Mild hybrid SUV', image: 'xc40' },];
export function ReelCards() { const ref = useRef<HTMLDivElement>(null); const { previousButtonProps, nextButtonProps, reelProps } = useReel({ ref });
return ( <div> <section ref={ref} className="reel gap-x-gridded-element" aria-label="Car models" {...reelProps} > {cards.map(({ title, subtitle, image }) => ( <div key={title} className="snap-start flex-col flex-shrink-0 bg-secondary p-24 shape-default gap-8" style={{ width: 'min(65vw, 16rem)' }} > <img className="img aspect-3/2 object-contain" src={`/images/cars/thumbnails/${image}.avif`} width="400" height="300" alt={`Volvo ${title}`} /> <p className="font-20 font-medium">{title}</p> <p className="text-secondary font-14">{subtitle}</p> </div> ))} </section> <div className="flex gap-16 mt-24 justify-end"> <IconButton icon="chevron-back" variant="filled" color="subtle" {...previousButtonProps} /> <IconButton icon="chevron-forward" variant="filled" color="subtle" {...nextButtonProps} /> </div> </div> );}Hidden scrollbar
Section titled “Hidden scrollbar”Add the .scrollbar-none utility class to hide the native scrollbar. Only use this when you provide other visual indicators (like navigation arrows) that more content is available off-screen.
import { useReel } from '@volvo-cars/react-headless';import { IconButton } from '@volvo-cars/react-icons';import { useRef } from 'react';
export function ReelHiddenScrollbar() { const ref = useRef<HTMLDivElement>(null); const { previousButtonProps, nextButtonProps, reelProps } = useReel({ ref });
return ( <div> <section ref={ref} className="reel gap-x-gridded-element scrollbar-none" aria-label="Scrollable frames" {...reelProps} > {Array.from({ length: 10 }, (_, i) => i + 1).map((frame) => ( <div key={frame} className="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style={{ minWidth: '12rem', minHeight: '8rem' }} > Frame {frame} </div> ))} </section> <div className="flex gap-16 mt-24 justify-end"> <IconButton icon="chevron-back" variant="filled" color="subtle" {...previousButtonProps} /> <IconButton icon="chevron-forward" variant="filled" color="subtle" {...nextButtonProps} /> </div> </div> );}CSS-only
Section titled “CSS-only”The .reel class provides horizontal scrolling with snap points without JavaScript. This is useful for server-rendered content or simple layouts that don’t need navigation controls.
<section class="reel gap-x-gridded-element" aria-label="Example frames"> <div class="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style="min-width: 12rem; min-height: 8rem" > Frame 1 </div> <div class="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style="min-width: 12rem; min-height: 8rem" > Frame 2 </div> <div class="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style="min-width: 12rem; min-height: 8rem" > Frame 3 </div> <div class="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style="min-width: 12rem; min-height: 8rem" > Frame 4 </div> <div class="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style="min-width: 12rem; min-height: 8rem" > Frame 5 </div> <div class="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style="min-width: 12rem; min-height: 8rem" > Frame 6 </div> <div class="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style="min-width: 12rem; min-height: 8rem" > Frame 7 </div> <div class="snap-start flex items-center justify-center bg-secondary p-24 shape-default" style="min-width: 12rem; min-height: 8rem" > Frame 8 </div></section>Keyboard navigation
Section titled “Keyboard navigation”- Arrow Left / Right — scroll through frames when the reel has focus.
- Tab — moves focus to focusable children within frames, or to the reel container itself if no focusable children exist.
- The
useReelhook automatically setstabIndex={0}on the reel container when it has no focusable children, ensuring keyboard users can scroll with arrow keys.
Accessibility
Section titled “Accessibility”Key consumer responsibilities from the WCAG audit:
- Labelling — provide
aria-labeloraria-labelledbyon the scrollable container so screen readers can announce its purpose. - Navigation buttons —
previousButtonPropsandnextButtonPropsincludearia-hidden="true"since the reel is already keyboard-navigable. - Focus management — with focusable children (links, buttons), keyboard users navigate by focusing each item. Without focusable children,
useReelmakes the container itself focusable. - Reduced motion — the
.reelclass respectsprefers-reduced-motionby disabling smooth scrolling.
See the useReel hooks documentation for full API reference, options, and return values.