# Defaults: ProductRail (/themes/sdk/reference/listing/defaults-3)

```ts
import { … } from '@poltura/theme-sdk/listing';
```

## `ProductRail` [#productrail]

A titled row of product cards with a "view all" link, as a grid or a scrolling row with arrows; nothing without cards.

```ts
ProductRail(props: ProductRailProps): ReactElement | null
```

**Props:** [`ProductRailProps`](/themes/sdk/reference/listing/types-3#productrailprops)

<Tab value='Example'>

```tsx
import { useProducts } from '@poltura/theme-sdk/data';
import { ProductRail } from '@poltura/theme-sdk/listing';

export function BestSellers() {
  const { data } = useProducts({ sort: 'popularity', limit: 8 });
  return <ProductRail title="Best sellers" cards={data?.items ?? []} layout="carousel" perView={4} />;
}
```

</Tab>
<Tab value='Source'>

```tsx title="source/product-rail.tsx"
import { useRef, type CSSProperties, type ReactElement } from 'react';
import { ArrowRight, Star } from 'lucide-react';
import { useTranslations } from '@poltura/theme-sdk/i18n';
import {
  ProductCard,
  ProductGrid,
  ProductList,
  type ProductCard as ProductCardView,
  type ProductRailProps,
} from '@poltura/theme-sdk/listing';
import { Link } from '@poltura/theme-sdk/routing';
import { Button, cn } from '@poltura/theme-sdk/ui';

const ARROW_PLACEMENT = { edges: 'edges', topRight: 'top-right' } as const;
const ARROW =
  'items-center justify-center rounded-full border border-border bg-background text-foreground';

type RailCarouselProps = {
  cards: readonly ProductCardView[];
  name: string;
  perView: number;
  mobileArrows: boolean;
  arrowPlacement: 'edges' | 'topRight';
};

function RailCarousel({
  cards,
  name,
  perView,
  mobileArrows,
  arrowPlacement,
}: RailCarouselProps): ReactElement {
  const t = useTranslations('common');
  const trackRef = useRef<HTMLDivElement>(null);
  const columns = Math.min(6, Math.max(3, Math.round(perView)));
  const fits = cards.length <= columns;
  const hasMobileArrows = mobileArrows && cards.length > 2;
  const showArrows = !fits || hasMobileArrows;
  const topRight = arrowPlacement === 'topRight';
  const arrowVisibility = cn(hasMobileArrows ? 'flex' : 'hidden', fits ? 'md:hidden' : 'md:flex');
  const arrowSize = hasMobileArrows ? 'h-11 w-11 md:h-10 md:w-10' : 'h-10 w-10';
  const trackStyle: CSSProperties & { '--rail-columns': string } = {
    '--rail-columns': String(columns),
  };

  const scrollByCard = (direction: number): void => {
    const track = trackRef.current;
    if (track === null) return;
    const card = track.querySelector<HTMLElement>('[data-rail-card]');
    const width = card?.getBoundingClientRect().width ?? track.clientWidth / columns;
    track.scrollBy({ left: direction * (width + 16), behavior: 'smooth' });
  };

  const arrow = (control: 'previous' | 'next'): ReactElement => (
    <button
      type='button'
      aria-label={control === 'previous' ? t('previous') : t('next')}
      data-rail-control={control}
      className={cn(
        topRight
          ? `inline-flex shrink-0 ${ARROW}`
          : control === 'previous'
            ? `absolute -left-4 top-1/3 z-10 ${ARROW}`
            : `absolute -right-4 top-1/3 z-10 ${ARROW}`,
        arrowSize,
        topRight ? null : arrowVisibility,
      )}
      onClick={() => scrollByCard(control === 'previous' ? -1 : 1)}
    >
      <span aria-hidden='true'>{control === 'previous' ? '‹' : '›'}</span>
    </button>
  );

  const previous = showArrows ? arrow('previous') : null;
  const next = showArrows ? arrow('next') : null;

  return (
    <div
      className={topRight ? 'flex flex-col' : 'relative'}
      data-rail-layout='carousel'
      data-per-view={String(columns)}
      data-rail-arrow-placement={ARROW_PLACEMENT[arrowPlacement]}
    >
      {showArrows && topRight ? (
        <div className={cn('mb-3 justify-end gap-2', arrowVisibility)} data-rail-controls=''>
          {previous}
          {next}
        </div>
      ) : null}
      {topRight ? null : previous}
      <ProductList cards={cards} name={name}>
        <div
          ref={trackRef}
          className='flex snap-x snap-mandatory gap-4 overflow-x-auto pb-2 [scrollbar-width:none]'
          style={trackStyle}
        >
          {cards.map((card) => (
            <div
              key={card.id}
              data-rail-card=''
              className='w-[calc(50%-0.5rem)] flex-none snap-start md:w-[calc(100%/var(--rail-columns)-1rem)]'
            >
              <ProductCard card={card} />
            </div>
          ))}
        </div>
      </ProductList>
      {topRight ? null : next}
    </div>
  );
}

export function ProductRail({
  cards,
  title,
  subtitle,
  viewAllHref = '/products',
  layout = 'grid',
  perView = 5,
  mobileArrows = false,
  arrowPlacement = 'edges',
  mobileViewAll = true,
  className,
  classes,
  icon,
}: ProductRailProps): ReactElement | null {
  const t = useTranslations('home');
  if (cards.length === 0) return null;
  const name = title ?? 'featured';
  return (
    <section className={cn('py-12', className)}>
      <div className='theme-container'>
        <div
          className={cn(
            'mb-8 flex flex-col justify-between gap-4 border-b border-border/50 pb-4 md:flex-row md:items-end',
            classes?.header,
          )}
        >
          <div>
            <div className='mb-2 flex items-center gap-2'>
              <div className='rounded-lg bg-primary/10 p-2 text-primary'>
                {icon ?? <Star className='h-5 w-5' />}
              </div>
              <h2 className='text-2xl font-bold tracking-tight text-foreground md:text-3xl'>
                {title}
              </h2>
            </div>
            {subtitle ? (
              <p className='ml-1 max-w-2xl text-sm font-medium text-muted-foreground md:text-base'>
                {subtitle}
              </p>
            ) : null}
          </div>
          <Link href={viewAllHref} className='hidden md:block'>
            <Button
              variant='ghost'
              className='gap-2 text-foreground hover:bg-primary/5 hover:text-primary'
            >
              {t('viewAll')}
              <ArrowRight className='h-4 w-4' />
            </Button>
          </Link>
        </div>
        <div className='mb-8'>
          {layout === 'carousel' ? (
            <RailCarousel
              cards={cards}
              name={name}
              perView={perView}
              mobileArrows={mobileArrows}
              arrowPlacement={arrowPlacement}
            />
          ) : (
            <ProductGrid
              cards={cards}
              listName={name}
              className={cn(
                'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 lg:gap-5 [&>:nth-child(odd):last-child:not(:only-child)]:hidden md:[&>:nth-child(odd):last-child:not(:only-child)]:block',
                classes?.grid,
              )}
            />
          )}
        </div>
        {mobileViewAll ? (
          <div className='featured-view-all-mobile text-center md:hidden' data-rail-view-all=''>
            <Link href={viewAllHref}>
              <Button variant='outline' className='w-full gap-2'>
                {t('viewAllProducts')}
                <ArrowRight className='h-4 w-4' />
              </Button>
            </Link>
          </div>
        ) : null}
      </div>
    </section>
  );
}
```

</Tab>


**See also:** [`ProductCard`](/themes/sdk/reference/listing/defaults-2#productcard)
