Skip to content

Reel

Edit on GitHub
@volvo-cars/css v2.3.0@volvo-cars/react-headless v0.24.7

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.

Frame 1
Frame 2
Frame 3
Frame 4
Frame 5
Frame 6
Frame 7
Frame 8
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>
);
}

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.

Frame 1
Frame 2
Frame 3
Frame 4
Frame 5
Frame 6
Frame 7
Frame 8
Frame 9
Frame 10
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>
);
}

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.

Volvo EX30

EX30

Electric SUV

Volvo EX90

EX90

Electric SUV

Volvo XC90

XC90

Plug-in hybrid SUV

Volvo XC60

XC60

Plug-in hybrid SUV

Volvo XC40

XC40

Mild hybrid SUV

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>
);
}

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.

Frame 1
Frame 2
Frame 3
Frame 4
Frame 5
Frame 6
Frame 7
Frame 8
Frame 9
Frame 10
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>
);
}

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.

Frame 1
Frame 2
Frame 3
Frame 4
Frame 5
Frame 6
Frame 7
Frame 8
<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>
  • 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 useReel hook automatically sets tabIndex={0} on the reel container when it has no focusable children, ensuring keyboard users can scroll with arrow keys.

Key consumer responsibilities from the WCAG audit:

  • Labelling — provide aria-label or aria-labelledby on the scrollable container so screen readers can announce its purpose.
  • Navigation buttonspreviousButtonProps and nextButtonProps include aria-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, useReel makes the container itself focusable.
  • Reduced motion — the .reel class respects prefers-reduced-motion by disabling smooth scrolling.

See the useReel hooks documentation for full API reference, options, and return values.