DEV Community

Cover image for Hero with logos + carousel
Felipe Menezes
Felipe Menezes

Posted on

Hero with logos + carousel

I built a simple hero section combining a logo marquee with a carousel.

It’s a common pattern in modern product pages. Logos for trust, carousel for showcasing visuals, but I wanted something easy to reuse and compose.

This was the result:

Block preview

Here’s the component code:


'use client'

import Image from 'next/image'
import Balancer from 'react-wrap-balancer'

import type { LogoMarqueeItem } from '@/components/flx/blocks/logos/logo-marquee/logo-marquee'
import { LogoMarquee } from '@/components/flx/blocks/logos/logo-marquee/logo-marquee'
import type { CtaProps } from '@/components/flx/blocks/shared/cta/cta'
import { Cta } from '@/components/flx/blocks/shared/cta/cta'
import {
  Carousel,
  CarouselContent,
  CarouselItem,
} from '@/components/ui/carousel'
import { cn } from '@/lib/utils'

export interface HeroLogosCarouselCarouselItem {
  title: string
  image: string
}

export interface HeroLogosCarouselProps {
  title: string
  description: string
  logosInfo: string
  primaryCTA?: CtaProps
  secondaryCTA?: CtaProps
  logos: LogoMarqueeItem[]
  carouselItems: HeroLogosCarouselCarouselItem[]
  className?: string
}

export function HeroLogosCarousel({
  title,
  description,
  logosInfo,
  primaryCTA,
  secondaryCTA,
  logos,
  carouselItems,
  className,
}: Readonly<HeroLogosCarouselProps>) {
  const containerWidthClassName = cn('w-full max-w-6xl mx-auto px-4', className)

  return (
    <section className="space-y-15">
      <div
        className={cn(
          'flex min-h-70 max-w-2xl flex-col justify-center gap-4',
          containerWidthClassName,
        )}
      >
        {title ? (
          <h1 className="text-2xl font-medium tracking-tight md:text-4xl">
            <Balancer balance={0.5}>{title}</Balancer>
          </h1>
        ) : null}
        {description ? (
          <p className="text-muted-foreground max-w-2xl text-base whitespace-pre-line">
            <Balancer balance={0.5}>{description}</Balancer>
          </p>
        ) : null}
        <div className="mt-3 flex flex-col gap-3 sm:flex-row">
          {primaryCTA ? (
            <Cta cta={primaryCTA} className="w-full sm:w-fit" />
          ) : null}
          {secondaryCTA ? (
            <Cta cta={secondaryCTA} className="w-full sm:w-fit" />
          ) : null}
        </div>
      </div>

      {logos?.length || carouselItems?.length ? (
        <div className="flex flex-col gap-10">
          <div className={containerWidthClassName}>
            <div className="flex flex-col gap-4 overflow-hidden sm:flex-row sm:items-center sm:justify-center">
              <p className="text-muted-foreground text-center text-sm sm:max-w-45 sm:text-left">
                {logosInfo}
              </p>
              <div className="w-full min-w-0 flex-1 sm:w-auto">
                <LogoMarquee items={logos} />
              </div>
            </div>
          </div>
          <div className="relative w-full">
            <Carousel
              opts={{
                align: 'center',
                loop: false,
                startIndex: Math.floor(carouselItems.length / 2),
              }}
              className="w-full"
              aria-label="Image carousel"
            >
              <CarouselContent
                className={cn(
                  'h-auto select-none first:!pl-0',
                  containerWidthClassName,
                )}
              >
                {carouselItems.map((item, index) => (
                  <CarouselItem
                    key={`${item.title}-${index}`}
                    className="basis-full pl-4 md:basis-1/2 lg:basis-2/3"
                  >
                    <div className="bg-muted rounded-lg p-4">
                      <div className="relative aspect-video w-full overflow-hidden rounded-lg">
                        <Image
                          src={item.image}
                          alt={item.title}
                          fill
                          className="object-cover"
                          unoptimized
                        />
                      </div>
                    </div>
                  </CarouselItem>
                ))}
              </CarouselContent>
            </Carousel>
          </div>
        </div>
      ) : null}
    </section>
  )
}

Enter fullscreen mode Exit fullscreen mode

Usage example below:


import {
  HeroLogosCarousel,
  type HeroLogosCarouselProps,
} from './hero-logos-carousel'

export function HeroLogosCarouselExample() {
  const values = {
    title: 'Ambitious ideas deserve world-class design',
    description:
      'We partner with startups to craft products that stand out, win trust, and scale with speed.',
    logosInfo: 'Our design crew worked with industry leaders',
    primaryCTA: {
      ctaEnabled: true,
      text: 'Get Started',
      link: '/',
      variant: 'default',
    },
    secondaryCTA: {
      ctaEnabled: true,
      text: 'View Work',
      link: '/',
      variant: 'secondary',
    },
    logos: [
      {
        title: 'Supabase',
        url: 'https://cdn.brandfetch.io/idsSceG8fK/w/800/h/156/theme/dark/logo.png?c=1dxbfHSJFAPEGdCLU4o5B',
      },
      {
        title: 'Google',
        url: 'https://cdn.brandfetch.io/id6O2oGzv-/theme/dark/logo.svg?c=1dxbfHSJFAPEGdCLU4o5B',
      },
      {
        title: 'Shopify',
        url: 'https://cdn.brandfetch.io/idAgPm7IvG/theme/dark/logo.svg?c=1dxbfHSJFAPEGdCLU4o5B',
      },
      {
        title: 'Mongo',
        url: 'https://cdn.brandfetch.io/ideyyfT0Lp/theme/dark/logo.svg?c=1dxbfHSJFAPEGdCLU4o5B',
      },
      {
        title: 'LottieFiles',
        url: 'https://cdn.brandfetch.io/idEExqEvR9/theme/dark/logo.svg?c=1dxbfHSJFAPEGdCLU4o5B',
      },
    ],
    carouselItems: [
      {
        title: 'Image 1',
        image:
          'https://images.unsplash.com/photo-1729575846511-f499d2e17d79?q=80&w=1332&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
      },
      {
        title: 'Image 2',
        image:
          'https://images.unsplash.com/photo-1679193559674-860ef78899bc?q=80&w=1198&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
      },
      {
        title: 'Image 3',
        image:
          'https://images.unsplash.com/photo-1771732266970-f63e35c8f47a?q=80&w=1332&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
      },
    ],
  } satisfies HeroLogosCarouselProps

  return (
    <HeroLogosCarousel
      title={values.title}
      description={values.description}
      logosInfo={values.logosInfo}
      primaryCTA={values.primaryCTA}
      secondaryCTA={values.secondaryCTA}
      logos={values.logos}
      carouselItems={values.carouselItems}
    />
  )
}

Enter fullscreen mode Exit fullscreen mode

you can preview it here

Top comments (0)